Make WordPress Core

Changeset 63251


Ignore:
Timestamp:
08/12/2026 04:35:03 PM (3 days ago)
Author:
desrosj
Message:

Media: Prevent loading images into Imagick which might be PostScript.

Props ehtis, dmsnell, jeremyfelt, westonruter, SergeyBiryukov, aaroncampbell, jorbin, batmoo, johnbillion, lancewillett, tyxla, vortfu, xknown.

Merges [63210] into the 5.0 branch.

Location:
branches/5.0
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • branches/5.0

  • branches/5.0/src/wp-includes/class-wp-image-editor-imagick.php

    r41288 r63251  
    7272                        'flopimage',
    7373                        'readimage',
     74                        'readimageblob',
    7475                );
    7576
     
    131132                        return true;
    132133
    133                 if ( ! is_file( $this->file ) && ! preg_match( '|^https?://|', $this->file ) )
     134                $is_stream = preg_match( '|^https?://|', $this->file );
     135                $is_file   = ! $is_stream && is_file( $this->file );
     136
     137                // Only allow loading files or HTTP streams.
     138                if ( ! $is_file && ! $is_stream )
    134139                        return new WP_Error( 'error_loading_image', __('File doesn’t exist?'), $this->file );
     140
     141                // Establish the provided filename based on the kind of resource being loaded.
     142                $given_filename = $this->file;
     143                if ( 0 === strncasecmp( $given_filename, 'file://', 7 ) ) {
     144                        $given_filename = basename( substr( $given_filename, 7 ) ); // 7 is the strlen of 'file://'.
     145                } elseif ( 1 === preg_match( '~^https?://~i', $this->file ) ) {
     146                        /*
     147                         * For URLs, it will be the final path segment.
     148                         *
     149                         * Example:
     150                         *
     151                         *     https://wordpress.org/i/happy.png?size=40px
     152                         *                             ╰───────╯
     153                         *                                this is the given filename
     154                         *
     155                         * If the stream returns a `Content-Disposition` header it would
     156                         * provide an alternative name, but this is used as a reasonable
     157                         * proxy to avoid adding the additional complexity of reading and
     158                         * parsing the returned HTTP headers.
     159                         */
     160                        $url_path = wp_parse_url( $this->file, PHP_URL_PATH );
     161
     162                        // This URL can not be parsed, so it is not a valid image resource.
     163                        if ( false === $url_path ) {
     164                                return new WP_Error( 'error_loading_image', __( 'File is not an image.' ), $this->file );
     165                        }
     166
     167                        /**
     168                         * The URL has an empty path, so continue with an empty string.
     169                         *
     170                         * This is the case with a URL such as `https://example.com?file_id=123`
     171                         */
     172                        if ( null === $url_path ) {
     173                                $url_path = '';
     174                        }
     175
     176                        $last_path_at   = strrpos( $url_path, '/' );
     177                        $given_filename = is_int( $last_path_at ) ? substr( $url_path, $last_path_at + 1 ) : $url_path;
     178                        $given_filename = rawurldecode( $given_filename );
     179                }
     180
     181                /*
     182                 * Strip off any potential `Imagick` format specifiers.
     183                 *
     184                 * If a real file exists with the identified format specifier, then
     185                 * `Imagick` may not treat it as a format, but WordPress will reject
     186                 * it anyway to avoid adding more complexity into this detection.
     187                 *
     188                 * `Imagick` reads only the first `FORMAT:` specifier on a name, but
     189                 * stripping a segment would promote a second specifier to the front
     190                 * of the name handed to `Imagick`, which would then honor it.
     191                 *
     192                 * Loop to capture all format specifiers for comparison.
     193                 *
     194                 * Exclude Windows drive-letter prefixes from here.
     195                 */
     196                $imagick_formats = array();
     197                while (
     198                        false !== ( $format_ends_at = strpos( $given_filename, ':' ) ) &&
     199                        1 !== preg_match( '~^[a-z]:~i', $given_filename )
     200                ) {
     201                        $imagick_formats[] = strtoupper( substr( $given_filename, 0, $format_ends_at ) );
     202                        $given_filename    = substr( $given_filename, $format_ends_at + 1 );
     203                }
     204
     205                $file_extension = strtolower( pathinfo( $given_filename, PATHINFO_EXTENSION ) );
    135206
    136207                /*
     
    140211                wp_raise_memory_limit( 'image' );
    141212
     213                /**
     214                 * Read the resource header for MIME sniffing.
     215                 *
     216                 * For files, which will be passed into Imagick by their file names, avoid
     217                 * eagerly loading the entire contents into PHP memory. For streams, however,
     218                 * it’s more important to avoid validating a separate copy of the file data
     219                 * than is later fetched by Imagick, so go ahead and load the entire payload,
     220                 * then pass it to Imagick as the data blob itself.
     221                 *
     222                 * @link https://mimesniff.spec.whatwg.org/#reading-the-resource-header
     223                 */
     224                try {
     225                        if ( $is_file ) {
     226                                $file_data = file_get_contents( $this->file, false, null, 0, 1445 );
     227                        } else {
     228                                $file_data = file_get_contents( $this->file );
     229                        }
     230                } catch ( Exception $e ) {
     231                        $file_data = false;
     232                }
     233                if ( false === $file_data ) {
     234                        return new WP_Error( 'error_loading_image', __( 'File doesn’t exist?' ), $this->file );
     235                }
     236
     237                $pdf_extensions = array(
     238                        'ai',
     239                        'epdf',
     240                        'pdf',
     241                        'pdfa',
     242                        'pocketmod',
     243                );
     244
     245                // Reject files claiming to be PDFs which lack the required signature.
     246                $has_pdf_extension = in_array( $file_extension, $pdf_extensions, true );
     247                $has_pdf_signature = 0 === strncmp( $file_data, '%PDF-', 5 );
     248                if ( $has_pdf_extension && ! $has_pdf_signature ) {
     249                        return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
     250                }
     251
     252                $ps_formats = array(
     253                        'DPS',
     254                        'EPI',
     255                        'EPS',
     256                        'EPSF',
     257                        'EPSI',
     258                        'PS',
     259                        'WPG',
     260                );
     261
     262                $ps_extensions = array(
     263                        'dps',
     264                        'epi',
     265                        'eps',
     266                        'eps2',
     267                        'eps3',
     268                        'epsf',
     269                        'epsi',
     270                        'ept',
     271                        'ept2',
     272                        'ept3',
     273                        'ps',
     274                        'ps2',
     275                        'ps3',
     276                        'wpg',
     277                );
     278
     279                // Reject files which Imagick will parse as PostScript.
     280                if (
     281                        array() !== array_intersect( $imagick_formats, $ps_formats ) ||
     282                        in_array( $file_extension, $ps_extensions, true ) ||
     283                        0 === strncmp( $file_data, '%!', 2 ) ||
     284                        0 === strncmp( $file_data, "\x04%!", 3 ) ||
     285                        0 === strncmp( $file_data, "\xC5\xD0\xD3\xC6", 4 ) ||
     286                        0 === strncmp( $file_data, "\xFFWPC", 3 )
     287                ) {
     288                        return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
     289                }
     290
     291                $compressed_extensions = array(
     292                        'gz',
     293                        'bz2',
     294                        'svgz',
     295                        'z',
     296                        'wmz',
     297                );
     298
     299                // Reject compressed archives that Imagick will transparently decompress.
     300                if (
     301                        in_array( $file_extension, $compressed_extensions, true ) ||
     302                        0 === strncmp( $file_data, "\x1F\x8B\x08", 3 ) || // gzip
     303                        0 === strncmp( $file_data, 'BZh', 3 ) || // bzip2
     304                        0 === strncmp( $file_data, "\x1F\x9D", 2 ) // compress
     305                ) {
     306                        return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
     307                }
     308
    142309                try {
    143310                        $this->image = new Imagick();
    144                         $file_extension = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) );
    145                         $filename = $this->file;
    146 
    147                         if ( 'pdf' == $file_extension ) {
     311                        $filename    = $this->file;
     312
     313                        if ( $has_pdf_signature ) {
    148314                                $filename = $this->pdf_setup();
    149315                        }
    150316
    151                         // Reading image after Imagick instantiation because `setResolution`
    152                         // only applies correctly before the image is read.
    153                         $this->image->readImage( $filename );
     317                        if ( $is_file ) {
     318                                $this->image->readImage( $filename );
     319                        } else {
     320                                $this->image->readImageBlob( $file_data, $given_filename );
     321                        }
    154322
    155323                        if ( ! $this->image->valid() )
Note: See TracChangeset for help on using the changeset viewer.