diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 828c96615f..6b6a044bff 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -2268,15 +2268,15 @@ function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) { return compact( 'ext', 'type', 'proper_filename' ); } + $real_mime = false; + // Validate image types. if ( $type && 0 === strpos( $type, 'image/' ) ) { // Attempt to figure out what type of image it actually is $real_mime = wp_get_image_mime( $file ); - if ( ! $real_mime ) { - $type = $ext = false; - } elseif ( $real_mime != $type ) { + if ( $real_mime && $real_mime != $type ) { /** * Filters the list mapping image mime types to their respective extensions. * @@ -2307,18 +2307,29 @@ function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) { $ext = $wp_filetype['ext']; $type = $wp_filetype['type']; } else { - $type = $ext = false; + // Reset $real_mime and try validating again. + $real_mime = false; } } - } elseif ( function_exists( 'finfo_file' ) ) { - // Use finfo_file if available to validate non-image files. + } + + // Validate files that didn't get validated during previous checks. + if ( $type && ! $real_mime && extension_loaded( 'fileinfo' ) ) { $finfo = finfo_open( FILEINFO_MIME_TYPE ); $real_mime = finfo_file( $finfo, $file ); finfo_close( $finfo ); - // If the extension does not match the file's real type, return false. - if ( $real_mime !== $type ) { - $type = $ext = false; + /* + * If $real_mime doesn't match what we're expecting, we need to do some extra + * vetting of application mime types to make sure this type of file is allowed. + * Other mime types are assumed to be safe, but should be considered unverified. + */ + if ( $real_mime && ( $real_mime !== $type ) && ( 0 === strpos( $real_mime, 'application' ) ) ) { + $allowed = get_allowed_mime_types(); + + if ( ! in_array( $real_mime, $allowed ) ) { + $type = $ext = false; + } } } diff --git a/tests/phpunit/data/uploads/dashicons.woff b/tests/phpunit/data/uploads/dashicons.woff new file mode 100644 index 0000000000..a13f9cf649 Binary files /dev/null and b/tests/phpunit/data/uploads/dashicons.woff differ diff --git a/tests/phpunit/data/uploads/pages-to-word.docx b/tests/phpunit/data/uploads/pages-to-word.docx new file mode 100644 index 0000000000..291052c0af Binary files /dev/null and b/tests/phpunit/data/uploads/pages-to-word.docx differ diff --git a/tests/phpunit/data/uploads/video-play.svg b/tests/phpunit/data/uploads/video-play.svg new file mode 100644 index 0000000000..b5ea206dde --- /dev/null +++ b/tests/phpunit/data/uploads/video-play.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/phpunit/tests/functions.php b/tests/phpunit/tests/functions.php index 5f4d6fd975..3ad52351f9 100644 --- a/tests/phpunit/tests/functions.php +++ b/tests/phpunit/tests/functions.php @@ -898,4 +898,165 @@ class Tests_Functions extends WP_UnitTestCase { $unique_uuids = array_unique( $uuids ); $this->assertEquals( $uuids, $unique_uuids ); } + + /** + * @ticket 39550 + * @dataProvider _wp_check_filetype_and_ext_data + */ + function test_wp_check_filetype_and_ext( $file, $filename, $expected ) { + if ( ! extension_loaded( 'fileinfo' ) ) { + $this->markTestSkipped( 'The fileinfo PHP extension is not loaded.' ); + } + + $this->assertEquals( $expected, wp_check_filetype_and_ext( $file, $filename ) ); + } + + /** + * @ticket 39550 + */ + function test_wp_check_filetype_and_ext_with_filtered_svg() { + if ( ! extension_loaded( 'fileinfo' ) ) { + $this->markTestSkipped( 'The fileinfo PHP extension is not loaded.' ); + } + + if ( is_multisite() ) { + $this->markTestSkipped( 'Test does not run in multisite' ); + } + + $file = DIR_TESTDATA . '/uploads/video-play.svg'; + $filename = 'video-play.svg'; + + $expected = array( + 'ext' => 'svg', + 'type' => 'image/svg+xml', + 'proper_filename' => false, + ); + + add_filter( 'upload_mimes', array( $this, '_filter_mime_types_svg' ) ); + $this->assertEquals( $expected, wp_check_filetype_and_ext( $file, $filename ) ); + + // Cleanup. + remove_filter( 'upload_mimes', array( $this, '_test_add_mime_types_svg' ) ); + } + + /** + * @ticket 39550 + */ + function test_wp_check_filetype_and_ext_with_filtered_woff() { + if ( ! extension_loaded( 'fileinfo' ) ) { + $this->markTestSkipped( 'The fileinfo PHP extension is not loaded.' ); + } + + if ( is_multisite() ) { + $this->markTestSkipped( 'Test does not run in multisite' ); + } + + $file = DIR_TESTDATA . '/uploads/dashicons.woff'; + $filename = 'dashicons.woff'; + + $expected = array( + 'ext' => 'woff', + 'type' => 'application/font-woff', + 'proper_filename' => false, + ); + + add_filter( 'upload_mimes', array( $this, '_filter_mime_types_woff' ) ); + $this->assertEquals( $expected, wp_check_filetype_and_ext( $file, $filename ) ); + + // Cleanup. + remove_filter( 'upload_mimes', array( $this, '_test_add_mime_types_woff' ) ); + } + + public function _filter_mime_types_svg( $mimes ) { + $mimes['svg'] = 'image/svg+xml'; + return $mimes; + } + + public function _filter_mime_types_woff( $mimes ) { + $mimes['woff'] = 'application/font-woff'; + return $mimes; + } + + public function _wp_check_filetype_and_ext_data() { + $data = array( + // Standard image. + array( + DIR_TESTDATA . '/images/canola.jpg', + 'canola.jpg', + array( + 'ext' => 'jpg', + 'type' => 'image/jpeg', + 'proper_filename' => false, + ), + ), + // Image with wrong extension. + array( + DIR_TESTDATA . '/images/test-image-mime-jpg.png', + 'test-image-mime-jpg.png', + array( + 'ext' => 'jpg', + 'type' => 'image/jpeg', + 'proper_filename' => 'test-image-mime-jpg.jpg', + ), + ), + // Image without extension. + array( + DIR_TESTDATA . '/images/test-image-no-extension', + 'test-image-no-extension', + array( + 'ext' => false, + 'type' => false, + 'proper_filename' => false, + ), + ), + // Valid non-image file with an image extension. + array( + DIR_TESTDATA . '/formatting/big5.txt', + 'big5.jpg', + array( + 'ext' => 'jpg', + 'type' => 'image/jpeg', + 'proper_filename' => false, + ), + ), + // Non-image file not allowed. + array( + DIR_TESTDATA . '/export/crazy-cdata.xml', + 'crazy-cdata.xml', + array( + 'ext' => false, + 'type' => false, + 'proper_filename' => false, + ), + ), + ); + + // Test a few additional file types on single sites. + if ( ! is_multisite() ) { + $data = array_merge( $data, array( + // Standard non-image file. + array( + DIR_TESTDATA . '/formatting/big5.txt', + 'big5.txt', + array( + 'ext' => 'txt', + 'type' => 'text/plain', + 'proper_filename' => false, + ), + ), + // Non-image file with wrong sub-type. + array( + DIR_TESTDATA . '/uploads/pages-to-word.docx', + 'pages-to-word.docx', + array( + 'ext' => 'docx', + 'type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'proper_filename' => false, + ), + ), + ) ); + } + + return $data; + } }