upload = new UploadTestHandler;
$this->setMwGlobals( 'wgHooks', [
'InterwikiLoadPrefix' => [
function ( $prefix, &$data ) {
return false;
}
],
] );
}
/**
* First checks the return code
* of UploadBase::getTitle() and then the actual returned title
*
* @dataProvider provideTestTitleValidation
* @covers UploadBase::getTitle
*/
public function testTitleValidation( $srcFilename, $dstFilename, $code, $msg ) {
/* Check the result code */
$this->assertEquals( $code,
$this->upload->testTitleValidation( $srcFilename ),
"$msg code" );
/* If we expect a valid title, check the title itself. */
if ( $code == UploadBase::OK ) {
$this->assertEquals( $dstFilename,
$this->upload->getTitle()->getText(),
"$msg text" );
}
}
/**
* Test various forms of valid and invalid titles that can be supplied.
*/
public static function provideTestTitleValidation() {
return [
/* Test a valid title */
[ 'ValidTitle.jpg', 'ValidTitle.jpg', UploadBase::OK,
'upload valid title' ],
/* A title with a slash */
[ 'A/B.jpg', 'A-B.jpg', UploadBase::OK,
'upload title with slash' ],
/* A title with illegal char */
[ 'A:B.jpg', 'A-B.jpg', UploadBase::OK,
'upload title with colon' ],
/* Stripping leading File: prefix */
[ 'File:C.jpg', 'C.jpg', UploadBase::OK,
'upload title with File prefix' ],
/* Test illegal suggested title (r94601) */
[ '%281%29.JPG', null, UploadBase::ILLEGAL_FILENAME,
'illegal title for upload' ],
/* A title without extension */
[ 'A', null, UploadBase::FILETYPE_MISSING,
'upload title without extension' ],
/* A title with no basename */
[ '.jpg', null, UploadBase::MIN_LENGTH_PARTNAME,
'upload title without basename' ],
/* A title that is longer than 255 bytes */
[ str_repeat( 'a', 255 ) . '.jpg', null, UploadBase::FILENAME_TOO_LONG,
'upload title longer than 255 bytes' ],
/* A title that is longer than 240 bytes */
[ str_repeat( 'a', 240 ) . '.jpg', null, UploadBase::FILENAME_TOO_LONG,
'upload title longer than 240 bytes' ],
];
}
/**
* Test the upload verification functions
* @covers UploadBase::verifyUpload
*/
public function testVerifyUpload() {
/* Setup with zero file size */
$this->upload->initializePathInfo( '', '', 0 );
$result = $this->upload->verifyUpload();
$this->assertEquals( UploadBase::EMPTY_FILE,
$result['status'],
'upload empty file' );
}
// Helper used to create an empty file of size $size.
private function createFileOfSize( $size ) {
$filename = $this->getNewTempFile();
$fh = fopen( $filename, 'w' );
ftruncate( $fh, $size );
fclose( $fh );
return $filename;
}
/**
* @covers UploadBase::verifyUpload
*
* test uploading a 100 bytes file with $wgMaxUploadSize = 100
*
* This method should be abstracted so we can test different settings.
*/
public function testMaxUploadSize() {
$this->setMwGlobals( [
'wgMaxUploadSize' => 100,
'wgFileExtensions' => [
'txt',
],
] );
$filename = $this->createFileOfSize( 100 );
$this->upload->initializePathInfo( basename( $filename ) . '.txt', $filename, 100 );
$result = $this->upload->verifyUpload();
$this->assertEquals(
[ 'status' => UploadBase::OK ],
$result
);
}
/**
* @covers UploadBase::checkSvgScriptCallback
* @dataProvider provideCheckSvgScriptCallback
*/
public function testCheckSvgScriptCallback( $svg, $wellFormed, $filterMatch, $message ) {
list( $formed, $match ) = $this->upload->checkSvgString( $svg );
$this->assertSame( $wellFormed, $formed, $message . " (well-formed)" );
$this->assertSame( $filterMatch, $match, $message . " (filter match)" );
}
public static function provideCheckSvgScriptCallback() {
// phpcs:disable Generic.Files.LineLength
return [
// html5sec SVG vectors
[
'',
true, /* SVG is well formed */
true, /* Evil SVG detected */
'Script tag in svg (http://html5sec.org/#47)'
],
[
'',
true,
true,
'SVG with onload property (http://html5sec.org/#11)'
],
[
'',
true,
true,
'SVG with onload property (http://html5sec.org/#65)'
],
[
'',
true,
true,
'SVG with javascript xlink (http://html5sec.org/#87)'
],
[
'',
true,
true,
'SVG with Opera image xlink (http://html5sec.org/#88 - c)'
],
[
'',
true,
true,
'SVG with Opera animation xlink (http://html5sec.org/#88 - a)'
],
[
'',
true,
true,
'SVG with Opera animation xlink (http://html5sec.org/#88 - b)'
],
[
'',
true,
true,
'SVG with Opera image xlink (http://html5sec.org/#88 - c)'
],
[
'',
true,
true,
'SVG with Opera foreignObject xlink (http://html5sec.org/#88 - d)'
],
[
'',
true,
true,
'SVG with Opera foreignObject xlink (http://html5sec.org/#88 - e)'
],
[
'',
true,
true,
'SVG with event handler set (http://html5sec.org/#89 - a)'
],
[
'',
true,
true,
'SVG with event handler animate (http://html5sec.org/#89 - a)'
],
[
'',
true,
true,
'SVG with element handler (http://html5sec.org/#94)'
],
[
'',
true,
true,
'SVG with href to data: url (http://html5sec.org/#95)'
],
[
'',
true,
true,
'SVG with Tiny handler (http://html5sec.org/#104)'
],
[
'',
true,
true,
'SVG with new CSS styles properties (http://html5sec.org/#109)'
],
[
'',
true,
true,
'SVG with new CSS styles properties as attributes'
],
[
'',
true,
true,
'SVG with new CSS styles properties as attributes (2)'
],
[
'',
true,
true,
'SVG with path marker-start (http://html5sec.org/#110)'
],
[
' ]> ',
false,
true,
'SVG with embedded stylesheet (http://html5sec.org/#125)'
],
[
' ',
true,
true,
'SVG with embedded stylesheet no doctype'
],
[
'',
true,
true,
'SVG with handler attribute (http://html5sec.org/#127)'
],
[
// Haven't found a browser that accepts this particular example, but we
// don't want to allow embeded svgs, ever
'',
true,
true,
'SVG with image filter via style (http://html5sec.org/#129)'
],
[
// This doesn't seem possible without embedding the svg, but just in case
'',
true,
true,
'SVG with animate from (http://html5sec.org/#137)'
],
[
'',
true,
true,
'SVG with animate xlink:href (http://html5sec.org/#137)'
],
[
'',
true,
true,
'SVG with animate y:href (http://html5sec.org/#137)'
],
// Other hostile SVG's
[
' ',
true,
true,
'SVG with non-local image href (T67839)'
],
[
' ',
true,
true,
'SVG with remote stylesheet (T59550)'
],
[
'',
true,
true,
'SVG with rembeded iframe (T62771)'
],
[
'',
true,
true,
'SVG with @import in style element (T71008)'
],
[
'',
true,
true,
'SVG with @import in style element and child element (T71008#c11)'
],
[
'',
true,
true,
'SVG with case-insensitive @import in style element (bug T85349)'
],
[
'',
true,
true,
'SVG with remote background image (T71008)'
],
[
'',
true,
true,
'SVG with remote background image, encoded (T71008)'
],
[
'',
true,
true,
'SVG with remote background image, in style element (T71008)'
],
[
// This currently doesn't seem to work in any browsers, but in case
// https://www.w3.org/TR/css3-images/ is implemented for SVG files
'',
true,
true,
'SVG with remote background image using image() (T71008)'
],
[
// As reported by Cure53
'',
true,
true,
'SVG with data:text/html link target (firefox only)'
],
[
' ]> ',
false,
true,
'SVG with encoded script tag in internal entity (reported by Beyond Security)'
],
[
' ]> ',
false,
false,
'SVG with external entity'
],
[
// The base64 = . If for some reason
// entities actually do get loaded, this should trigger
// filterMatch to be true. So this test verifies that we
// are not loading external entities.
' ]> ',
false,
false, /* False verifies entities aren't getting loaded */
'SVG with data: uri external entity'
],
[
"",
true,
true,
'SVG with javascript link with newline (T122653)'
],
// Test good, but strange files that we want to allow
[
'',
true,
false,
'SVG with link to a remote site'
],
[
'',
true,
false,
'SVG with local urls, including filter: in style'
],
[
' ]> ',
false,
false,
'SVG with evil default attribute values'
],
[
'',
true,
true,
'SVG with an evil external dtd'
],
[
'',
true,
true,
'SVG with random public doctype'
],
[
'',
true,
true,
'SVG with random SYSTEM doctype'
],
[
'] >',
false,
false,
'SVG with parameter entity'
],
[
'',
false,
false,
'SVG with entity referencing parameter entity'
],
[
' ] >',
false,
false,
'SVG with long entity'
],
[
' ] >',
true,
false,
'SVG with apostrophe quote entity'
],
[
' ] >',
false,
false,
'SVG with recursive entity',
],
[
' ]> ',
true, /* well-formed */
false, /* filter-hit */
'GraphViz-esque svg with #FIXED xlink ns (Should be allowed)'
],
[
' ]> ',
false,
false,
'GraphViz ATLIST exception should match exactly'
],
[
' ]>',
true,
false,
'DTD with comments (Should be allowed)'
],
[
' ]>',
false,
false,
'DTD with invalid comment'
],
[
' ]>',
false,
false,
'DTD with invalid comment 2'
],
[
' ]>',
true,
false,
'DTD with aliased entities (Should be allowed)'
],
[
' ]>',
true,
false,
'DTD with aliased entities apos (Should be allowed)'
],
[
'',
true,
false,
'SVG with local filter (T69044)'
],
[
'',
true,
true,
'SVG with non-local filter (T69044)'
],
];
// phpcs:enable
}
/**
* @covers UploadBase::detectScriptInSvg
* @dataProvider provideDetectScriptInSvg
*/
public function testDetectScriptInSvg( $svg, $expected, $message ) {
// This only checks some weird cases, most tests are in testCheckSvgScriptCallback() above
$result = $this->upload->detectScriptInSvg( $svg, false );
$this->assertSame( $expected, $result, $message );
}
public static function provideDetectScriptInSvg() {
global $IP;
return [
[
"$IP/tests/phpunit/data/upload/buggynamespace-original.svg",
false,
'SVG with a weird but valid namespace definition created by Adobe Illustrator'
],
[
"$IP/tests/phpunit/data/upload/buggynamespace-okay.svg",
false,
'SVG with a namespace definition created by Adobe Illustrator and mangled by Inkscape'
],
[
"$IP/tests/phpunit/data/upload/buggynamespace-okay2.svg",
false,
'SVG with a namespace definition created by Adobe Illustrator and mangled by Inkscape (twice)'
],
[
"$IP/tests/phpunit/data/upload/buggynamespace-bad.svg",
[ 'uploadscriptednamespace', 'i' ],
'SVG with a namespace definition using an undefined entity'
],
[
"$IP/tests/phpunit/data/upload/buggynamespace-evilhtml.svg",
[ 'uploadscriptednamespace', 'http://www.w3.org/1999/xhtml' ],
'SVG with an html namespace encoded as an entity'
],
];
}
/**
* @covers UploadBase::checkXMLEncodingMissmatch
* @dataProvider provideCheckXMLEncodingMissmatch
*/
public function testCheckXMLEncodingMissmatch( $fileContents, $evil ) {
$filename = $this->getNewTempFile();
file_put_contents( $filename, $fileContents );
$this->assertSame( $evil, UploadBase::checkXMLEncodingMissmatch( $filename ) );
}
public function provideCheckXMLEncodingMissmatch() {
return [
[ '', true ],
[ '', false ],
[ '', false ],
];
}
/**
* @covers UploadBase::detectScript
* @dataProvider provideDetectScript
*/
public function testDetectScript( $filename, $mime, $extension, $expected, $message ) {
$result = $this->upload->detectScript( $filename, $mime, $extension );
$this->assertSame( $expected, $result, $message );
}
public static function provideDetectScript() {
global $IP;
return [
[
"$IP/tests/phpunit/data/upload/png-plain.png",
'image/png',
'png',
false,
'PNG with no suspicious things in it, should pass.'
],
[
"$IP/tests/phpunit/data/upload/png-embedded-breaks-ie5.png",
'image/png',
'png',
true,
'PNG with embedded data that IE5/6 interprets as HTML; should be rejected.'
],
[
"$IP/tests/phpunit/data/upload/jpeg-a-href-in-metadata.jpg",
'image/jpeg',
'jpeg',
false,
'JPEG with innocuous HTML in metadata from a flickr photo; should pass (T27707).'
],
];
}
}
class UploadTestHandler extends UploadBase {
public function initializeFromRequest( &$request ) {
}
public function testTitleValidation( $name ) {
$this->mTitle = false;
$this->mDesiredDestName = $name;
$this->mTitleError = UploadBase::OK;
$this->getTitle();
return $this->mTitleError;
}
/**
* Almost the same as UploadBase::detectScriptInSvg, except it's
* public, works on an xml string instead of filename, and returns
* the result instead of interpreting them.
*/
public function checkSvgString( $svg ) {
$check = new XmlTypeCheck(
$svg,
[ $this, 'checkSvgScriptCallback' ],
false,
[
'processing_instruction_handler' => [ UploadBase::class, 'checkSvgPICallback' ],
'external_dtd_handler' => [ UploadBase::class, 'checkSvgExternalDTD' ],
]
);
return [ $check->wellFormed, $check->filterMatch ];
}
/**
* Same as parent function, but override visibility to 'public'.
*/
public function detectScriptInSvg( $filename, $partial ) {
return parent::detectScriptInSvg( $filename, $partial );
}
}