Make WordPress Core


Ignore:
Timestamp:
10/27/2021 02:58:24 PM (3 years ago)
Author:
johnjamesjacoby
Message:

Admin/HTTP API: add suggested filename support to download_url().

This change allows for external clients to supply a suggested filename via a Content-Disposition response header. This filename is processed through sanitize_file_name() to ensure it is allowable (on the server, MIME's, etc...) and validate_file() to prevent directory traversal.

If the suggested filename fails the above processing/checks, that suggestion is discarded and the standard temporary filename (generated by WordPress) is used.

If no Content-Disposition header is found in the response headers, the standard temporary filename continues to be used as per normal.

Included in this change are 6 additional PHPUnit tests with 9 assertions. These tests confirm that valid filename values are correctly saved, and invalid filename values are correctly rejected.

Props cklosows, costdev, dd32, johnjamesjacoby, ocean90, psrpinto.

Fixes #38231.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/admin/includesFile.php

    r51639 r51939  
    7777    public function __return_5() {
    7878        return 5;
     79    }
     80
     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        );
    79250    }
    80251
Note: See TracChangeset for help on using the changeset viewer.