Make WordPress Core

Ticket #38231: 38231.7.diff

File 38231.7.diff, 7.5 KB (added by johnjamesjacoby, 3 years ago)

Use sanitize_file_name() and validate_file() - props @costdev (comment imminent)

  • src/wp-admin/includes/file.php

    diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php
    index 50345c6331..8a28c63adc 100644
    a b function wp_handle_sideload( &$file, $overrides = false, $time = null ) { 
    11121112 *
    11131113 * @since 2.5.0
    11141114 * @since 5.2.0 Signature Verification with SoftFail was added.
     1115 * @since 5.9.0 Support for Content-Disposition filename was added.
    11151116 *
    11161117 * @param string $url                    The URL of the file to download.
    11171118 * @param int    $timeout                The timeout for the request to download the file.
    function download_url( $url, $timeout = 300, $signature_verification = false ) { 
    11821183                return new WP_Error( 'http_404', trim( wp_remote_retrieve_response_message( $response ) ), $data );
    11831184        }
    11841185
     1186        $content_disposition = wp_remote_retrieve_header( $response, 'content-disposition' );
     1187
     1188        if ( $content_disposition ) {
     1189                $content_disposition = strtolower( $content_disposition );
     1190
     1191                if ( 0 === strpos( $content_disposition, 'attachment; filename=' ) ) {
     1192                        $tmpfname_disposition = sanitize_file_name( substr( $content_disposition, 21 ) );
     1193                } else {
     1194                        $tmpfname_disposition = '';
     1195                }
     1196
     1197                // Potential file name must be valid string
     1198                if ( $tmpfname_disposition && is_string( $tmpfname_disposition ) && ( 0 === validate_file( $tmpfname_disposition ) ) ) {
     1199                        if ( rename( $tmpfname, $tmpfname_disposition ) ) {
     1200                                $tmpfname = $tmpfname_disposition;
     1201                        }
     1202
     1203                        if ( ( $tmpfname !== $tmpfname_disposition ) && file_exists( $tmpfname_disposition ) ) {
     1204                                unlink( $tmpfname_disposition );
     1205                        }
     1206                }
     1207        }
     1208
    11851209        $content_md5 = wp_remote_retrieve_header( $response, 'content-md5' );
    11861210
    11871211        if ( $content_md5 ) {
  • tests/phpunit/tests/admin/includesFile.php

    diff --git a/tests/phpunit/tests/admin/includesFile.php b/tests/phpunit/tests/admin/includesFile.php
    index 5929940955..db17f834b5 100644
    a b class Tests_Admin_IncludesFile extends WP_UnitTestCase { 
    7878                return 5;
    7979        }
    8080
     81        /**
     82         * @ticket 38231
     83         * @dataProvider data_download_url_should_respect_filename_from_content_disposition_header
     84         *
     85         * @covers ::download_url
     86         *
     87         * @param $filter  A callback containing a fake Content-Disposition header.
     88         */
     89        public function test_download_url_should_respect_filename_from_content_disposition_header( $filter ) {
     90                add_filter( 'pre_http_request', array( $this, $filter ), 10, 3 );
     91
     92                $filename = download_url( 'url_with_content_disposition_header' );
     93                $this->assertStringContainsString( 'filename-from-content-disposition-header', $filename );
     94                $this->assertFileExists( $filename );
     95                $this->unlink( $filename );
     96
     97                remove_filter( 'pre_http_request', array( $this, $filter ) );
     98        }
     99
     100        /**
     101         * Data provider for test_download_url_should_respect_filename_from_content_disposition_header.
     102         *
     103         * @return array
     104         */
     105        public function data_download_url_should_respect_filename_from_content_disposition_header() {
     106                return array(
     107                        'valid parameters' => array( 'filter_content_disposition_header_with_filename' ),
     108                        'path traversal'   => array( 'filter_content_disposition_header_with_filename_with_path_traversal' ),
     109                        'no quotes'        => array( 'filter_content_disposition_header_with_filename_without_quotes' ),
     110                );
     111        }
     112
     113        /**
     114         * Filter callback for data_download_url_should_respect_filename_from_content_disposition_header.
     115         *
     116         * @since 5.9.0
     117         *
     118         * @return array
     119         */
     120        public function filter_content_disposition_header_with_filename( $response, $args, $url ) {
     121                return array(
     122                        'response' => array(
     123                                'code' => 200,
     124                        ),
     125                        'headers'  => array(
     126                                'content-disposition' => 'attachment; filename="filename-from-content-disposition-header.txt"',
     127                        ),
     128                );
     129        }
     130
     131        /**
     132         * Filter callback for data_download_url_should_respect_filename_from_content_disposition_header.
     133         *
     134         * @since 5.9.0
     135         *
     136         * @return array
     137         */
     138        public function filter_content_disposition_header_with_filename_with_path_traversal( $response, $args, $url ) {
     139                return array(
     140                        'response' => array(
     141                                'code' => 200,
     142                        ),
     143                        'headers'  => array(
     144                                'content-disposition' => 'attachment; filename="../../filename-from-content-disposition-header.txt"',
     145                        ),
     146                );
     147        }
     148
     149        /**
     150         * Filter callback for data_download_url_should_respect_filename_from_content_disposition_header.
     151         *
     152         * @since 5.9.0
     153         *
     154         * @return array
     155         */
     156        public function filter_content_disposition_header_with_filename_without_quotes( $response, $args, $url ) {
     157                return array(
     158                        'response' => array(
     159                                'code' => 200,
     160                        ),
     161                        'headers'  => array(
     162                                'content-disposition' => 'attachment; filename=filename-from-content-disposition-header.txt',
     163                        ),
     164                );
     165        }
     166
     167        /**
     168         * @ticket 38231
     169         * @dataProvider data_download_url_should_reject_filename_from_invalid_content_disposition_header
     170         *
     171         * @covers ::download_url
     172         *
     173         * @param $filter  A callback containing a fake Content-Disposition header.
     174         */
     175        public function test_download_url_should_reject_filename_from_invalid_content_disposition_header( $filter ) {
     176                add_filter( 'pre_http_request', array( $this, $filter ), 10, 3 );
     177
     178                $filename = download_url( 'url_with_content_disposition_header' );
     179                $this->assertStringContainsString( 'url_with_content_disposition_header', $filename );
     180                $this->unlink( $filename );
     181
     182                remove_filter( 'pre_http_request', array( $this, $filter ) );
     183        }
     184
     185        /**
     186         * Data provider for test_download_url_should_reject_filename_from_invalid_content_disposition_header.
     187         *
     188         * @return array
     189         */
     190        public function data_download_url_should_reject_filename_from_invalid_content_disposition_header() {
     191                return array(
     192                        'no context'        => array( 'filter_content_disposition_header_with_filename_without_context' ),
     193                        'inline context'    => array( 'filter_content_disposition_header_with_filename_with_inline_context' ),
     194                        'form-data context' => array( 'filter_content_disposition_header_with_filename_with_form_data_context' ),
     195                );
     196        }
     197
     198        /**
     199         * Filter callback for data_download_url_should_reject_filename_from_invalid_content_disposition_header.
     200         *
     201         * @since 5.9.0
     202         *
     203         * @return array
     204         */
     205        public function filter_content_disposition_header_with_filename_without_context( $response, $args, $url ) {
     206                return array(
     207                        'response' => array(
     208                                'code' => 200,
     209                        ),
     210                        'headers'  => array(
     211                                'content-disposition' => 'filename="filename-from-content-disposition-header.txt"',
     212                        ),
     213                );
     214        }
     215
     216        /**
     217         * Filter callback for data_download_url_should_reject_filename_from_invalid_content_disposition_header.
     218         *
     219         * @since 5.9.0
     220         *
     221         * @return array
     222         */
     223        public function filter_content_disposition_header_with_filename_with_inline_context( $response, $args, $url ) {
     224                return array(
     225                        'response' => array(
     226                                'code' => 200,
     227                        ),
     228                        'headers'  => array(
     229                                'content-disposition' => 'inline; filename="filename-from-content-disposition-header.txt"',
     230                        ),
     231                );
     232        }
     233
     234        /**
     235         * Filter callback for data_download_url_should_reject_filename_from_invalid_content_disposition_header.
     236         *
     237         * @since 5.9.0
     238         *
     239         * @return array
     240         */
     241        public function filter_content_disposition_header_with_filename_with_form_data_context( $response, $args, $url ) {
     242                return array(
     243                        'response' => array(
     244                                'code' => 200,
     245                        ),
     246                        'headers'  => array(
     247                                'content-disposition' => 'form-data; name="file"; filename="filename-from-content-disposition-header.txt"',
     248                        ),
     249                );
     250        }
     251
    81252        /**
    82253         * Verify that a WP_Error object is returned when invalid input is passed as the `$url` parameter.
    83254         *