diff --git src/wp-includes/functions.php src/wp-includes/functions.php
index 2720f590af..69e1319a84 100644
--- src/wp-includes/functions.php
+++ src/wp-includes/functions.php
@@ -2023,6 +2023,35 @@ function _wp_upload_dir( $time = null ) {
 }
 
 /**
+ * Assign a new extension to a filename.
+ *
+ * @since 4.9
+ *
+ * @param string $filename The original filename.
+ * @param string $ext      The new extension.
+ * @return string The renamed file.
+ */
+function wp_update_filename_extension( $filename, $ext ) {
+	$ext = strtolower( $ext );
+	$ext = rtrim( $ext, '.' );
+	$ext = ltrim( $ext, '.' );
+
+	$filename_parts = explode( '.', $filename );
+
+	// Remove the old extension.
+	if ( count( $filename_parts ) > 1 ) {
+		array_pop( $filename_parts );
+	}
+
+	// Add the new extension.
+	if ( strlen( $ext ) ) {
+		$filename_parts[] = $ext;
+	}
+
+	return implode( '.', $filename_parts );
+}
+
+/**
  * Get a filename that is sanitized and unique for the given directory.
  *
  * If the filename is not unique, then a number will be added to the filename
@@ -2267,12 +2296,12 @@ function wp_check_filetype( $filename, $mimes = null ) {
 function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) {
 	$proper_filename = false;
 
-	// Do basic extension validation and MIME mapping
+	// Do basic extension validation and MIME mapping.
 	$wp_filetype = wp_check_filetype( $filename, $mimes );
 	$ext = $wp_filetype['ext'];
 	$type = $wp_filetype['type'];
 
-	// We can't do any further validation without a file to work with
+	// We can't do any further validation without a file to work with.
 	if ( ! file_exists( $file ) ) {
 		return compact( 'ext', 'type', 'proper_filename' );
 	}
@@ -2282,7 +2311,7 @@ function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) {
 	// Validate image types.
 	if ( $type && 0 === strpos( $type, 'image/' ) ) {
 
-		// Attempt to figure out what type of image it actually is
+		// Attempt to figure out what type of image it actually is.
 		$real_mime = wp_get_image_mime( $file );
 
 		if ( $real_mime && $real_mime != $type ) {
@@ -2294,22 +2323,20 @@ function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) {
 			 * @param  array $mime_to_ext Array of image mime types and their matching extensions.
 			 */
 			$mime_to_ext = apply_filters( 'getimagesize_mimes_to_exts', array(
-				'image/jpeg' => 'jpg',
-				'image/png'  => 'png',
-				'image/gif'  => 'gif',
-				'image/bmp'  => 'bmp',
-				'image/tiff' => 'tif',
+				'image/jpeg'      => 'jpg',
+				'image/png'       => 'png',
+				'image/gif'       => 'gif',
+				'image/bmp'       => 'bmp',
+				'image/x-ms-bmp'  => 'bmp',
+				'image/tiff'      => 'tif',
 			) );
 
-			// Replace whatever is after the last period in the filename with the correct extension
+			// Rename the file with the correct extension.
 			if ( ! empty( $mime_to_ext[ $real_mime ] ) ) {
-				$filename_parts = explode( '.', $filename );
-				array_pop( $filename_parts );
-				$filename_parts[] = $mime_to_ext[ $real_mime ];
-				$new_filename = implode( '.', $filename_parts );
+				$new_filename = wp_update_filename_extension( $filename, $mime_to_ext[ $real_mime ] );
 
 				if ( $new_filename != $filename ) {
-					$proper_filename = $new_filename; // Mark that it changed
+					$proper_filename = $new_filename; // Mark that it changed.
 				}
 				// Redefine the extension / MIME
 				$wp_filetype = wp_check_filetype( $new_filename, $mimes );
@@ -2323,21 +2350,63 @@ function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) {
 	}
 
 	// Validate files that didn't get validated during previous checks.
-	if ( $type && ! $real_mime && extension_loaded( 'fileinfo' ) ) {
+	if (
+		$type &&
+		! $real_mime &&
+		extension_loaded( 'fileinfo' ) &&
+		defined( 'FILEINFO_MIME_TYPE' )
+	) {
 		$finfo = finfo_open( FILEINFO_MIME_TYPE );
 		$real_mime = finfo_file( $finfo, $file );
 		finfo_close( $finfo );
 
 		/*
 		 * 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.
+		 * vetting of greylisted 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 ( $real_mime && ( $real_mime !== $type ) ) {
+			// Get the true file extension for a greylisted filetype.
+			$greylist = get_greylisted_mime_types();
+			$real_mime = strtolower( sanitize_mime_type( $real_mime ) );
+			$real_ext = false;
+			foreach ( $greylist as $exts => $greylist_types ) {
+				if ( in_array( $real_mime, $greylist_types, true ) ) {
+					$real_ext = $exts;
+					break;
+				}
+			}
+
+			if ( $real_ext ) {
+				// This MIME type is greylisted, so make sure the extension is allowed.
+				$allowed = get_allowed_mime_types();
+				$found = false;
+				foreach ( $allowed as $exts => $allowed_type ) {
+					$exts = explode( '|', $exts );
+					if ( in_array( $real_ext, $exts, true ) ) {
+						// Rename the file with the correct extension.
+						if ( $ext !== $real_ext ) {
+							$new_filename = wp_update_filename_extension( $filename, $real_ext );
+
+							if ( $new_filename != $filename ) {
+								$proper_filename = $new_filename; // Mark that it changed.
+							}
+
+							$ext = $real_ext;
+						}
 
-			if ( ! in_array( $real_mime, $allowed ) ) {
-				$type = $ext = false;
+						// Always prefer the MIME type from get_allowed_mimes().
+						$type = $allowed_type;
+
+						$found = true;
+						break;
+					}
+				}
+
+				// Unauthorized file.
+				if ( ! $found ) {
+					$ext = $type = false;
+				}
 			}
 		}
 	}
@@ -2546,6 +2615,73 @@ function wp_get_ext_types() {
 }
 
 /**
+ * Retrieve list of greylisted mime types and file extensions.
+ *
+ * These are file types deserving of extra validation during uploads. Unlike
+ * get_allowed_mime_types, each entry consists of a single file extension
+ * with multiple MIME types.
+ *
+ * @since 4.9
+ *
+ * @return array Array of mime types keyed by the file extension corresponding
+ *               to those types.
+ */
+function get_greylisted_mime_types() {
+	$greylist = array(
+		'air' => array(
+			'application/adobe.air-application-installer-package+zip',
+			'application/vnd.adobe.air-application-installer-package+zip',
+			'application/x-adobe.air-application-installer-package+zip',
+		),
+		'fla' => array(
+			'application/dtg.local.flash',
+			'application/vnd.dtg.local.flash',
+			'application/x-dtg.local.flash',
+		),
+		'flv' => array(
+			'application/flash-video',
+			'application/vnd.flash-video',
+			'application/x-flash-video',
+			'flv-application/octet-stream',
+			'video/flv',
+			'video/x-flv',
+		),
+		'swf' => array(
+			'application/adobe.flash.movie',
+			'application/futuresplash',
+			'application/shockwave-flash',
+			'application/vnd.adobe.flash.movie',
+			'application/vnd.futuresplash',
+			'application/vnd.shockwave-flash',
+			'application/x-adobe.flash.movie',
+			'application/x-futuresplash',
+			'application/x-shockwave-flash',
+		),
+		'spl' => array(
+			'application/adobe.flash.movie',
+			'application/futuresplash',
+			'application/shockwave-flash',
+			'application/vnd.adobe.flash.movie',
+			'application/vnd.futuresplash',
+			'application/vnd.shockwave-flash',
+			'application/x-adobe.flash.movie',
+			'application/x-futuresplash',
+			'application/x-shockwave-flash',
+		),
+	);
+
+	/**
+	 * Filters the greylist results.
+	 *
+	 * @since 4.9
+	 *
+	 * @param array $greylist Array of mime types keyed by the file extension corresponding
+	 *                        to those types.
+	 */
+	return apply_filters( 'get_greylisted_mime_types', $greylist );
+}
+
+/**
  * Retrieve list of allowed mime types and file extensions.
  *
  * @since 2.8.6
diff --git tests/phpunit/data/uploads/test.swf tests/phpunit/data/uploads/test.swf
new file mode 100644
index 0000000000000000000000000000000000000000..c6195c41eebe7b3b2006eff39a268320e973feac
GIT binary patch
literal 140
zcmZ<@4`%OSU|^_VV2x*B;9tPNz{AL3&zuVs>d;|eVaQD_E>28OWk@bcO)N<bNv$Yx
z%S_ElVJHEz7(7yQa`F|z^NVs)6d9&zF|Zo}wXvrF2{s@G12}`75y<CYU<V0sFxrYw
UV*>JIrm-+^FmW*ZGdKW+0pm9pO8@`>

literal 0
HcmV?d00001

diff --git tests/phpunit/tests/functions.php tests/phpunit/tests/functions.php
index 7c8736ab67..778071b6e3 100644
--- tests/phpunit/tests/functions.php
+++ tests/phpunit/tests/functions.php
@@ -122,6 +122,13 @@ class Tests_Functions extends WP_UnitTestCase {
 		);
 	}
 
+	/**
+	 * @dataProvider _wp_update_filename_extension
+	 */
+	function test_wp_update_filename_extension( $filename, $ext, $expected ) {
+		$this->assertEquals( $expected, wp_update_filename_extension( $filename, $ext ) );
+	}
+
 	function test_wp_unique_filename() {
 
 		$testdir = DIR_TESTDATA . '/images/';
@@ -1044,6 +1051,27 @@ class Tests_Functions extends WP_UnitTestCase {
 	}
 
 	/**
+	 * @ticket 39550
+	 *
+	 * @dataProvider _wp_check_filetype_and_ext_with_filtered_swf
+	 */
+	function test_wp_check_filetype_and_ext_with_filtered_swf( $file, $filename, $expected ) {
+		if ( ! extension_loaded( 'fileinfo' ) ) {
+			$this->markTestSkipped( 'The fileinfo PHP extension is not loaded.' );
+		}
+
+		if ( is_multisite() ) {
+			$this->markTestSkipped( 'Test does not run in multisite' );
+		}
+
+		add_filter( 'upload_mimes', array( $this, '_filter_mime_types_swf' ) );
+		$this->assertEquals( $expected, wp_check_filetype_and_ext( $file, $filename ) );
+
+		// Cleanup.
+		remove_filter( 'upload_mimes', array( $this, '_test_add_mime_types_swf' ) );
+	}
+
+	/**
 	 * Data profider for test_wp_get_image_mime();
 	 */
 	public function _wp_get_image_mime() {
@@ -1130,6 +1158,26 @@ class Tests_Functions extends WP_UnitTestCase {
 					'proper_filename' => false,
 				),
 			),
+			// Flash file with invalid extension.
+			array(
+				DIR_TESTDATA . '/uploads/test.swf',
+				'big5.jpg',
+				array(
+					'ext' => false,
+					'type' => false,
+					'proper_filename' => false,
+				),
+			),
+			// Flash file with valid extension.
+			array(
+				DIR_TESTDATA . '/uploads/test.swf',
+				'test.swf',
+				array(
+					'ext' => false,
+					'type' => false,
+					'proper_filename' => false,
+				),
+			),
 		);
 
 		// Test a few additional file types on single sites.
@@ -1161,4 +1209,62 @@ class Tests_Functions extends WP_UnitTestCase {
 		return $data;
 	}
 
+	public function _filter_mime_types_swf( $mimes ) {
+		$mimes['swf'] = 'application/x-shockwave-flash';
+		return $mimes;
+	}
+
+	public function _wp_update_filename_extension() {
+		$data = array(
+			array(
+				'test.jpg',
+				'png',
+				'test.png',
+			),
+			array(
+				'test',
+				'png',
+				'test.png',
+			),
+			array(
+				'test',
+				'.png',
+				'test.png',
+			),
+			array(
+				'test.jpg',
+				'',
+				'test',
+			),
+		);
+
+		return $data;
+	}
+
+	public function _wp_check_filetype_and_ext_with_filtered_swf() {
+		$data = array(
+			// Correctly named SWF.
+			array(
+				DIR_TESTDATA . '/uploads/test.swf',
+				'test.swf',
+				array(
+					'ext' => 'swf',
+					'type' => 'application/x-shockwave-flash',
+					'proper_filename' => false,
+				),
+			),
+			// Incorrectly named SWF.
+			array(
+				DIR_TESTDATA . '/uploads/test.swf',
+				'test.jpg',
+				array(
+					'ext' => 'swf',
+					'type' => 'application/x-shockwave-flash',
+					'proper_filename' => 'test.swf',
+				),
+			),
+		);
+
+		return $data;
+	}
 }
