[wp-trac] [WordPress Trac] #65188: Uploading PNG images with a larger number of colors causes timeouts and eventually CPU starvation

WordPress Trac noreply at wordpress.org
Thu May 7 09:21:04 UTC 2026


#65188: Uploading PNG images with a larger number of colors causes timeouts and
eventually CPU starvation
---------------------------+-----------------------------
 Reporter:  romainmrhenry  |      Owner:  (none)
     Type:  defect (bug)   |     Status:  new
 Priority:  normal         |  Milestone:  Awaiting Review
Component:  Upload         |    Version:  6.8
 Severity:  normal         |   Keywords:
  Focuses:  performance    |
---------------------------+-----------------------------
 Since WordPress 6.8 there is new handling of PNG images.

 Specifically in `thumbnail_image` at `wp-includes/class-wp-image-editor-
 imagick.php:403`

 Uploading a png image with a large number of colors, for example a
 photographic image in .png format, causes the `getImageColors()` step at
 line 506 to be extremely slow.

 This seems to be the result of the chosen filter `FILTER_TRIANGLE`.


 A standalone version of this function with some extra log lines:

 {{{#!php
 <?php

 $image = new \Imagick( realpath( './foo.png' ) );

 thumbnail_image( $image, 2048, 2048 );

 function thumbnail_image( $image, $dst_w, $dst_h, $filter_name =
 'FILTER_TRIANGLE' ) {
         error_log( 'a' );
         $current_colors = $image->getImageColors();
         error_log( 'b' );
         error_log( $current_colors );

         $allowed_filters = array(
                 'FILTER_POINT',
                 'FILTER_BOX',
                 'FILTER_TRIANGLE',
                 'FILTER_HERMITE',
                 'FILTER_HANNING',
                 'FILTER_HAMMING',
                 'FILTER_BLACKMAN',
                 'FILTER_GAUSSIAN',
                 'FILTER_QUADRATIC',
                 'FILTER_CUBIC',
                 'FILTER_CATROM',
                 'FILTER_MITCHELL',
                 'FILTER_LANCZOS',
                 'FILTER_BESSEL',
                 'FILTER_SINC',
         );

         /**
          * Set the filter value if '$filter_name' name is in the allowed
 list and the related
          * Imagick constant is defined or fall back to the default filter.
          */
         if ( in_array( $filter_name, $allowed_filters, true ) && defined(
 'Imagick::' . $filter_name ) ) {
                 $filter = constant( 'Imagick::' . $filter_name );
         } else {
                 $filter = defined( 'Imagick::FILTER_TRIANGLE' ) ?
 Imagick::FILTER_TRIANGLE : false;
         }

         try {
                 $size = null;
                 try {
                         error_log( '1' );
                         $size = $image->getImageGeometry();
                         error_log( '2' );
                 } catch ( Exception $e ) {
                         return new WP_Error( 'invalid_image', __( 'Could
 not read image size.' ) );
                 }

                 /*
                         * To be more efficient, resample large images to
 5x the destination size before resizing
                         * whenever the output size is less that 1/3 of the
 original image size (1/3^2 ~= .111),
                         * unless we would be resampling to a scale smaller
 than 128x128.
                         */
                 if ( is_callable( array( $image, 'sampleImage' ) ) ) {
                         $resize_ratio  = ( $dst_w / $size['width'] ) * (
 $dst_h / $size['height'] );
                         $sample_factor = 5;

                         if ( $resize_ratio < .111 && ( $dst_w *
 $sample_factor > 128 && $dst_h * $sample_factor > 128 ) ) {
                                 error_log( '3' );
                                 $image->sampleImage( $dst_w *
 $sample_factor, $dst_h * $sample_factor );
                                 error_log( '4' );
                         }
                 }

                 /*
                         * Use resizeImage() when it's available and a
 valid filter value is set.
                         * Otherwise, fall back to the scaleImage() method
 for resizing, which
                         * results in better image quality over
 resizeImage() with default filter
                         * settings and retains backward compatibility with
 pre 4.5 functionality.
                         */
                 if ( is_callable( array( $image, 'resizeImage' ) ) &&
 $filter ) {
                         error_log( '5' );
                         $image->setOption( 'filter:support', '2.0' );
                         $image->resizeImage( $dst_w, $dst_h, $filter, 1 );
                         error_log( '6' );
                 } else {
                         error_log( '7' );
                         $image->scaleImage( $dst_w, $dst_h );
                         error_log( '8' );
                 }

                 error_log( '9' );
                 $image->setOption( 'png:compression-filter', '5' );
                 $image->setOption( 'png:compression-level', '9' );
                 $image->setOption( 'png:compression-strategy', '1' );
                 error_log( '10' );
                 // Check to see if a PNG is indexed, and find the pixel
 depth.
                 if ( is_callable( array( $image, 'getImageDepth' ) ) ) {
                         error_log( '11' );
                         $indexed_pixel_depth = $image->getImageDepth();
                         error_log( $indexed_pixel_depth );
                         error_log( '12' );

                         // Indexed PNG files get some additional handling.
                         if ( 0 < $indexed_pixel_depth && 8 >=
 $indexed_pixel_depth ) {
                                 // Check for an alpha channel.
                                 error_log( '13' );
                                 if (
                                         is_callable( array( $image,
 'getImageAlphaChannel' ) )
                                         && $image->getImageAlphaChannel()
                                 ) {
                                         $image->setOption( 'png:include-
 chunk', 'tRNS' );
                                 } else {
                                         $image->setOption( 'png:exclude-
 chunk', 'all' );
                                 }
                                 error_log( '14' );

                                 // Reduce colors in the images to maximum
 needed, using the global colorspace.
                                 $max_colors = pow( 2, $indexed_pixel_depth
 );
                                 error_log( $max_colors );
                                 if ( is_callable( array( $image,
 'getImageColors' ) ) ) {
                                         $current_colors =
 $image->getImageColors();
                                         error_log( $current_colors );
                                         $max_colors = min( $max_colors,
 $current_colors );
                                 }
                                 error_log( $max_colors );

                                 error_log( '15' );
                                 $image->quantizeImage( $max_colors,
 $image->getColorspace(), 0, false, false );
                                 error_log( '16' );

                                 error_log( 'c' );
                                 $current_colors =
 $image->getImageColors();
                                 error_log( 'd' );
                                 error_log( $current_colors );

                                 /**
                                  * If the colorspace is 'gray', use the
 png8 format to ensure it stays indexed.
                                  */
                                 if ( Imagick::COLORSPACE_GRAY ===
 $image->getImageColorspace() ) {
                                         $image->setOption( 'png:format',
 'png8' );
                                 }
                         }
                 }

                 /*
                         * If alpha channel is not defined, set it opaque.
                         *
                         * Note that Imagick::getImageAlphaChannel() is
 only available if Imagick
                         * has been compiled against ImageMagick version
 6.4.0 or newer.
                         */
                 if ( is_callable( array( $image, 'getImageAlphaChannel' )
 )
                         && is_callable( array( $image,
 'setImageAlphaChannel' ) )
                         && defined( 'Imagick::ALPHACHANNEL_UNDEFINED' )
                         && defined( 'Imagick::ALPHACHANNEL_OPAQUE' )
                 ) {
                         if ( $image->getImageAlphaChannel() ===
 Imagick::ALPHACHANNEL_UNDEFINED ) {
                                 $image->setImageAlphaChannel(
 Imagick::ALPHACHANNEL_OPAQUE );
                         }
                 }

                 // Limit the bit depth of resized images.
                 if ( is_callable( array( $image, 'getImageDepth' ) ) &&
 is_callable( array( $image, 'setImageDepth' ) ) ) {
                         /**
                          * Filters the maximum bit depth of resized
 images.
                          *
                          * This filter only applies when resizing using
 the Imagick editor since GD
                          * does not support getting or setting bit depth.
                          *
                          * Use this to adjust the maximum bit depth of
 resized images.
                          *
                          * @since 6.8.0
                          *
                          * @param int $max_depth   The maximum bit depth.
 Default is the input depth.
                          * @param int $image_depth The bit depth of the
 original image.
                          */
                         $max_depth = $image->getImageDepth();
                         $image->setImageDepth( $max_depth );
                 }
         } catch ( Exception $e ) {
                 return new WP_Error( 'image_resize_error',
 $e->getMessage() );
         }
 }
 }}}


 -------

 It also seems that there is some confusion around channel depth and colors
 in this function.
 The `$max_colors` is a value per channel while `$current_colors` is for
 all channels combined.

 I think `$max_colors` should be multiplied by 3.


 -----

 It is extremely easy to DOS a WordPress server that allows file uploads by
 feeding it photographic PNG images.

-- 
Ticket URL: <https://core.trac.wordpress.org/ticket/65188>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform


More information about the wp-trac mailing list