diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php
index 50345c6331..8a28c63adc 100644
--- a/src/wp-admin/includes/file.php
+++ b/src/wp-admin/includes/file.php
@@ -1112,6 +1112,7 @@ function wp_handle_sideload( &$file, $overrides = false, $time = null ) {
  *
  * @since 2.5.0
  * @since 5.2.0 Signature Verification with SoftFail was added.
+ * @since 5.9.0 Support for Content-Disposition filename was added.
  *
  * @param string $url                    The URL of the file to download.
  * @param int    $timeout                The timeout for the request to download the file.
@@ -1182,6 +1183,29 @@ function download_url( $url, $timeout = 300, $signature_verification = false ) {
 		return new WP_Error( 'http_404', trim( wp_remote_retrieve_response_message( $response ) ), $data );
 	}
 
+	$content_disposition = wp_remote_retrieve_header( $response, 'content-disposition' );
+
+	if ( $content_disposition ) {
+		$content_disposition = strtolower( $content_disposition );
+
+		if ( 0 === strpos( $content_disposition, 'attachment; filename=' ) ) {
+			$tmpfname_disposition = sanitize_file_name( substr( $content_disposition, 21 ) );
+		} else {
+			$tmpfname_disposition = '';
+		}
+
+		// Potential file name must be valid string
+		if ( $tmpfname_disposition && is_string( $tmpfname_disposition ) && ( 0 === validate_file( $tmpfname_disposition ) ) ) {
+			if ( rename( $tmpfname, $tmpfname_disposition ) ) {
+				$tmpfname = $tmpfname_disposition;
+			}
+
+			if ( ( $tmpfname !== $tmpfname_disposition ) && file_exists( $tmpfname_disposition ) ) {
+				unlink( $tmpfname_disposition );
+			}
+		}
+	}
+
 	$content_md5 = wp_remote_retrieve_header( $response, 'content-md5' );
 
 	if ( $content_md5 ) {
diff --git a/tests/phpunit/tests/admin/includesFile.php b/tests/phpunit/tests/admin/includesFile.php
index 5929940955..db17f834b5 100644
--- a/tests/phpunit/tests/admin/includesFile.php
+++ b/tests/phpunit/tests/admin/includesFile.php
@@ -78,6 +78,177 @@ class Tests_Admin_IncludesFile extends WP_UnitTestCase {
 		return 5;
 	}
 
+	/**
+	 * @ticket 38231
+	 * @dataProvider data_download_url_should_respect_filename_from_content_disposition_header
+	 *
+	 * @covers ::download_url
+	 *
+	 * @param $filter  A callback containing a fake Content-Disposition header.
+	 */
+	public function test_download_url_should_respect_filename_from_content_disposition_header( $filter ) {
+		add_filter( 'pre_http_request', array( $this, $filter ), 10, 3 );
+
+		$filename = download_url( 'url_with_content_disposition_header' );
+		$this->assertStringContainsString( 'filename-from-content-disposition-header', $filename );
+		$this->assertFileExists( $filename );
+		$this->unlink( $filename );
+
+		remove_filter( 'pre_http_request', array( $this, $filter ) );
+	}
+
+	/**
+	 * Data provider for test_download_url_should_respect_filename_from_content_disposition_header.
+	 *
+	 * @return array
+	 */
+	public function data_download_url_should_respect_filename_from_content_disposition_header() {
+		return array(
+			'valid parameters' => array( 'filter_content_disposition_header_with_filename' ),
+			'path traversal'   => array( 'filter_content_disposition_header_with_filename_with_path_traversal' ),
+			'no quotes'        => array( 'filter_content_disposition_header_with_filename_without_quotes' ),
+		);
+	}
+
+	/**
+	 * Filter callback for data_download_url_should_respect_filename_from_content_disposition_header.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @return array
+	 */
+	public function filter_content_disposition_header_with_filename( $response, $args, $url ) {
+		return array(
+			'response' => array(
+				'code' => 200,
+			),
+			'headers'  => array(
+				'content-disposition' => 'attachment; filename="filename-from-content-disposition-header.txt"',
+			),
+		);
+	}
+
+	/**
+	 * Filter callback for data_download_url_should_respect_filename_from_content_disposition_header.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @return array
+	 */
+	public function filter_content_disposition_header_with_filename_with_path_traversal( $response, $args, $url ) {
+		return array(
+			'response' => array(
+				'code' => 200,
+			),
+			'headers'  => array(
+				'content-disposition' => 'attachment; filename="../../filename-from-content-disposition-header.txt"',
+			),
+		);
+	}
+
+	/**
+	 * Filter callback for data_download_url_should_respect_filename_from_content_disposition_header.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @return array
+	 */
+	public function filter_content_disposition_header_with_filename_without_quotes( $response, $args, $url ) {
+		return array(
+			'response' => array(
+				'code' => 200,
+			),
+			'headers'  => array(
+				'content-disposition' => 'attachment; filename=filename-from-content-disposition-header.txt',
+			),
+		);
+	}
+
+	/**
+	 * @ticket 38231
+	 * @dataProvider data_download_url_should_reject_filename_from_invalid_content_disposition_header
+	 *
+	 * @covers ::download_url
+	 *
+	 * @param $filter  A callback containing a fake Content-Disposition header.
+	 */
+	public function test_download_url_should_reject_filename_from_invalid_content_disposition_header( $filter ) {
+		add_filter( 'pre_http_request', array( $this, $filter ), 10, 3 );
+
+		$filename = download_url( 'url_with_content_disposition_header' );
+		$this->assertStringContainsString( 'url_with_content_disposition_header', $filename );
+		$this->unlink( $filename );
+
+		remove_filter( 'pre_http_request', array( $this, $filter ) );
+	}
+
+	/**
+	 * Data provider for test_download_url_should_reject_filename_from_invalid_content_disposition_header.
+	 *
+	 * @return array
+	 */
+	public function data_download_url_should_reject_filename_from_invalid_content_disposition_header() {
+		return array(
+			'no context'        => array( 'filter_content_disposition_header_with_filename_without_context' ),
+			'inline context'    => array( 'filter_content_disposition_header_with_filename_with_inline_context' ),
+			'form-data context' => array( 'filter_content_disposition_header_with_filename_with_form_data_context' ),
+		);
+	}
+
+	/**
+	 * Filter callback for data_download_url_should_reject_filename_from_invalid_content_disposition_header.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @return array
+	 */
+	public function filter_content_disposition_header_with_filename_without_context( $response, $args, $url ) {
+		return array(
+			'response' => array(
+				'code' => 200,
+			),
+			'headers'  => array(
+				'content-disposition' => 'filename="filename-from-content-disposition-header.txt"',
+			),
+		);
+	}
+
+	/**
+	 * Filter callback for data_download_url_should_reject_filename_from_invalid_content_disposition_header.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @return array
+	 */
+	public function filter_content_disposition_header_with_filename_with_inline_context( $response, $args, $url ) {
+		return array(
+			'response' => array(
+				'code' => 200,
+			),
+			'headers'  => array(
+				'content-disposition' => 'inline; filename="filename-from-content-disposition-header.txt"',
+			),
+		);
+	}
+
+	/**
+	 * Filter callback for data_download_url_should_reject_filename_from_invalid_content_disposition_header.
+	 *
+	 * @since 5.9.0
+	 *
+	 * @return array
+	 */
+	public function filter_content_disposition_header_with_filename_with_form_data_context( $response, $args, $url ) {
+		return array(
+			'response' => array(
+				'code' => 200,
+			),
+			'headers'  => array(
+				'content-disposition' => 'form-data; name="file"; filename="filename-from-content-disposition-header.txt"',
+			),
+		);
+	}
+
 	/**
 	 * Verify that a WP_Error object is returned when invalid input is passed as the `$url` parameter.
 	 *
