TimThumb is dead - how to handle images in WordPress (2026 guide)

TimThumb is dead - how to handle images in WordPress (2026 guide)

Last verified: October 12, 2026
16 min read
Guide
Full-stack developer
Security auditor

TimThumb sat in almost every commercial WordPress theme around 2010. Drop one PHP file in the theme, pass src, w, and h on the query string, and you got a cropped JPEG without touching the Media Library. That convenience is why the script spread, and why the 2011 remote code execution wave hit so many sites at once.

In 2026 there is no reason to keep it. WordPress writes intermediate sizes at upload, browsers pick a width from srcset, and CDNs can transform a master file at the edge. If timthumb.php (or a renamed thumb.php) still lives under wp-content, treat it as an open door, not a feature.

#Why TimThumb died

TimThumb did not fail because cropping went out of fashion. It failed because the design assumed a theme could safely fetch, resize, and cache images from URLs while running as PHP on the same host that served the site.

The 2011 flaw let attackers abuse writable cache paths and weak validation so the script wrote executable content the web server would later run. Mark Maunder disclosed it on 1 August 2011 after his own blog was hit: the script only partly matched the source hostname against its whitelist, so a URL that merely contained an allowed domain could pull a PHP file into the cache directory, and a direct request then executed it. He also told site owners to grep their WordPress tree for base64_decode and long encoded strings, the usual footprint of injected code. Patch waves followed, then forks, then silence. Themes kept shipping copies under new filenames. Scanners still find them on inherited projects years later because nobody owned the cleanup once the marketing site launched.

Three structural problems never went away:

  1. Security. A public PHP endpoint that accepts a remote or local src and writes under a cache directory is a classic RCE surface when path checks slip.
  2. Performance. First request paid a PHP and GD (or Imagick) tax. Cache misses under traffic turned the origin into an image factory.
  3. No Media Library contract. TimThumb ignored attachment metadata. WordPress could not track crops, regenerate after a theme change, or emit responsive markup from a single source of truth.

Core media closed that gap. Uploads land under /wp-content/uploads/, WordPress records width and height, and registered sizes become real files next to the original. Display code reads those files. Nothing in the theme needs to open a URL, resize it, and hope the cache directory stays locked down.

If you are auditing a client handoff, treat any theme-level resize endpoint the same way you treat an open eval on $_GET: remove it, then prove images still resolve through attachments. Security scanners flag TimThumb by filename; attackers also probe renamed copies, so filename hygiene alone is not enough.

#TimThumb vulnerability timeline

The history fits in four dates, each backed by a primary source:

DateWhat happenedSource
August 2011Domain whitelist flaw in versions before 2.0, exploited in the wildNVD, CVE-2011-4106
August 2011Maunder rewrites the script as WordThumb, which is merged into TimThumb 2.0Mark Maunder’s blog
June 2014Remote command execution in TimThumb 2.8.13 and WordThumb 1.07 when the WebShot feature is enabledNVD, CVE-2014-4663
27 September 2014Author Ben Gillbanks announces he is dropping support and maintenanceBinaryMoon

CVE-2011-4106 was only published to the NVD in October 2013, but the official description records exploitation in August 2011. Maunder gauged the reach indirectly at the time: Google showed over 39 million results for the script name. His core diagnosis applies to any resizer that lives inside a theme:

“timthumb.php is inherently insecure because it relies on being able to write files into a directory that is accessible by people visiting your website.”

Mark Maunder, 1 August 2011

The 2011 patch closed that hole, not the design. Three years later the WebShot flaw passed shell metacharacters from the src parameter into a system command. Gillbanks wrote in his 2014 post that he had barely touched the code in years and that anyone still using TimThumb did so at their own risk. A theme that still ships the file depends on code nobody has maintained for more than a decade.

#How to register image sizes with add_image_size

WordPress ships default intermediates (thumbnail, medium, medium_large, large) and lets themes register more with add_image_size(). Those are the replacements for TimThumb query strings: declare the crops your layouts need, generate them once, serve static files forever after.

Register sizes on after_setup_theme:

function wppoland_setup_theme() {
    add_theme_support( 'post-thumbnails' );

    // Soft crop - keep aspect ratio, fit inside the box.
    add_image_size( 'blog-list', 800, 400 );

    // Hard crop - exact box from the center.
    add_image_size( 'team-member', 300, 300, true );

    // Hard crop with focus toward the top of the frame.
    add_image_size( 'hero-banner', 1920, 600, array( 'center', 'top' ) );
}
add_action( 'after_setup_theme', 'wppoland_setup_theme' );

In a template, prefer the attachment helpers so WordPress can attach classes, alt text, and responsive attributes:

if ( has_post_thumbnail() ) {
    the_post_thumbnail( 'hero-banner', array( 'class' => 'img-fluid' ) );
}

Or, when you already hold an attachment ID:

echo wp_get_attachment_image( $attachment_id, 'blog-list' );

Sizes registered after files were uploaded do not appear until you regenerate. On a small library the Regenerate Thumbnails plugin is enough. On anything large, use WP-CLI so the job can run without a browser timeout:

wp media regenerate --yes

The wp media regenerate documentation lists flags that keep the job small: --only-missing creates only the sizes an attachment lacks, --image_size=<name> rebuilds a single size, and --skip-delete keeps old intermediates on disk when something outside your control links to them:

wp media regenerate --only-missing --yes
wp media regenerate --image_size=hero-banner --yes

Keep the size list short. Every extra crop multiplies disk use. Register what the theme and content models actually render - list cards, author avatars, heroes - and drop dead names when a redesign retires a layout. You can inspect registered intermediates with wp_get_intermediate_image_sizes() when auditing a theme you did not write.

The win versus TimThumb is simple: generation happens at upload (or regenerate) time. Page views serve files from disk or CDN cache. Origin PHP is not in the resize path.

Editors should still upload a high-quality master that is large enough for the biggest crop you register. Soft crops cannot invent missing pixels. Hard crops need composition awareness: a headshot registered as center, top behaves differently from a landscape product shot cropped from the dead center. Document those choices in the theme README so the next developer does not reintroduce a TimThumb-style “just pass w and h” shortcut.

#What image sizes WordPress creates by default

Before adding sizes, count what core already writes per upload. The option defaults in the WordPress schema are:

NameDefault dimensionsSince
thumbnail150 x 150, hard cropMedia settings
mediumup to 300 x 300Media settings
medium_large768 wide, no height limitWordPress 4.4
largeup to 1024 x 1024Media settings
1536x1536up to 1536 x 1536WordPress 5.3
2048x2048up to 2048 x 2048WordPress 5.3

The last two come from the internal _wp_add_additional_image_sizes(), and the core comment advises against changing them because the names describe the dimensions. Every add_image_size() in the theme adds to this list, so one upload easily yields ten files or more.

WordPress 5.3 also added the big image threshold. When the original exceeds 2560 pixels in width or height, core writes a downscaled copy with a -scaled suffix and uses it as the “full” size; the original stays on disk and its filename is stored in the attachment metadata as original_image. Change or disable it with the big_image_size_threshold filter:

// Raise the threshold to 4000 px.
add_filter( 'big_image_size_threshold', fn() => 4000 );

// Disable it entirely.
add_filter( 'big_image_size_threshold', '__return_false' );

There is a matching cap for srcset: in current core code the max_srcset_image_width filter drops candidates wider than 2048 pixels. If a theme needs larger candidates for high-density screens, that filter is the knob to turn, not a resize script.

#How srcset and sizes work for WordPress images

Modern WordPress does not stop at one derivative. When you print an image through wp_get_attachment_image() or the_post_thumbnail(), core builds a srcset from available widths and a default sizes hint. The browser then downloads a file close to the display width instead of always pulling the hero crop on a phone.

Rough shape of what core emits:

<img
  src="image-800x400.jpg"
  srcset="image-300x150.jpg 300w,
          image-800x400.jpg 800w,
          image-1024x512.jpg 1024w"
  sizes="(max-width: 600px) 100vw, 800px"
  alt=""
/>

According to the WordPress 4.4 dev note that introduced the feature, the default sizes value is equivalent to (max-width: {width}px) 100vw, {width}px, and core leaves srcset and sizes alone when content HTML already carries them.

Your job as a theme developer is usually not to hand-write srcset. It is to:

  • Register sensible intermediates so the set has useful steps.
  • Pass an accurate sizes value when the default is wrong for a layout (full-bleed hero versus a third-column card).
  • Filter wp_calculate_image_sizes when a component always sits in a known CSS width.

TimThumb could not do this. It produced one URL per request. Responsive delivery depended on either shipping oversized files or inventing more query-string variants. Core metadata plus srcset is the maintained path, documented with the rest of WordPress media administration.

For LCP images, still be explicit: preload or mark the hero with fetchpriority="high" where appropriate, and avoid lazy-loading the first viewport image. Responsive markup helps bandwidth; it does not replace choosing a sane primary src.

#Which loading attributes WordPress adds to images

WordPress 5.5 started adding loading="lazy" to images, so offscreen files wait until the user scrolls toward them. WordPress 6.1 added decoding="async" to wp_get_attachment_image(), which lets the browser decode without blocking rendering. Since WordPress 6.3 one function decides all three attributes, wp_get_loading_optimization_attributes(). It gives the first large enough image fetchpriority="high" (the default floor is 50,000 pixels of area, filterable through wp_min_priority_img_pixels) and does not lazy-load that element, because no element should carry both. If your LCP image comes from a block or a CSS background the function never sees, mark it yourself.

When debugging “why is the phone still downloading the 1920px file,” inspect the rendered sizes attribute first. A wrong 100vw on a 320px-wide card forces the browser to pick a huge candidate even when smaller intermediates exist. Fix the hint; do not bring back a theme PHP resizer to paper over bad markup.

#How to resize WordPress images on a CDN

Native sizes cover most editorial layouts. Design systems, ad slots, and A/B heroes sometimes need widths you never registered. Putting that work back into a theme PHP script recreates TimThumb. Push it to the CDN instead.

Hosts and edge platforms commonly accept width, height, fit, and format on a URL or via an image worker in front of /wp-content/uploads/. Conceptually:

<img
  src="https://cdn.example.com/uploads/2026/09/hero.jpg?width=800&height=400&format=avif"
  alt=""
  width="800"
  height="400"
/>

On Cloudflare the transform goes straight into the path as https://<zone>/cdn-cgi/image/<options>/<source-image>, for example /cdn-cgi/image/width=400,format=auto/wp-content/uploads/2026/10/photo.jpg. Per the Cloudflare documentation it works on zones with transformations enabled, and format=auto serves the most efficient format the requesting browser supports. No Worker is needed for that; a Worker only earns its place when you want custom rules, such as rewriting every /wp-content/uploads/ URL automatically.

The origin keeps one master (or the WordPress full size). The edge caches each transform. You keep security boundaries: the CDN only reads objects you already published under uploads, not arbitrary remote src values from a query string on your theme.

When you adopt transforms:

  • Prefer one pipeline. Either WordPress intermediates for known crops, or CDN transforms for fluid sizes - not both fighting over the same markup.
  • Keep format negotiation at the edge (AVIF/WebP with JPEG fallback) so editors can still upload JPEG from cameras and phones.
  • Invalidate or version when you replace a master file so long-lived edge caches do not serve yesterday’s crop.

WordPress already accepts WebP (since 5.8) and AVIF (since 6.5) as upload formats. You do not need TimThumb-era optimizer plugins solely to invent a resize endpoint. Use core for library truth; use the CDN when you need elastic dimensions.

Measure after the switch. Compare bytes for the LCP image and the median content image before and after. If edge transforms add latency on first hit, warm the common widths or keep WordPress intermediates for the above-the-fold set and reserve the CDN for long-tail sizes. The goal is predictable delivery, not a clever URL grammar.

#How to serve WebP and AVIF images in WordPress

Format support depends on the server’s image library (Imagick or GD) as much as on the WordPress version. Tools > Site Health, Info tab, Media Handling section lists what your server can process. Upload an AVIF or WebP file and WordPress treats it like a JPEG. For existing JPEG workflows, the Performance Team’s Modern Image Formats plugin generates sub-sizes in AVIF (or WebP where AVIF is unsupported) and keeps the original.

The official core figures:

AVIF usually compresses better than WebP, especially on photos with smooth gradients. WebP still has slightly wider support in older browsers, so a common order is AVIF first and WebP as fallback.

#How to use the picture element with AVIF and WebP

For full control over which format a browser receives, write the <picture> element yourself:

<picture>
    <source srcset="image.avif" type="image/avif">
    <source srcset="image.webp" type="image/webp">
    <img src="image.jpg" alt="Image description">
</picture>

In most projects, letting WordPress and the CDN negotiate the format is easier to maintain.

#How to change image quality and output format in WordPress

Every sub-size WordPress writes goes through WP_Image_Editor, and two filters shape the result.

The first is quality. In the current source of the class the default is 82 for JPEG and 86 for WebP; other formats get 82. The wp_editor_set_quality filter receives the quality and MIME type, so each format can be tuned separately. For JPEG, core then runs the older jpeg_quality filter, which has the last word if a plugin uses it:

add_filter( 'wp_editor_set_quality', function( $quality, $mime_type ) {
    if ( 'image/avif' === $mime_type ) {
        return 70;
    }
    return $quality;
}, 10, 2 );

The second is the output format. image_editor_output_format maps each input MIME type to the format sub-sizes are saved in. The WordPress 6.5 dev note uses exactly this example to make JPEG uploads produce AVIF sub-sizes:

add_filter( 'image_editor_output_format', function( $formats ) {
    $formats['image/jpeg'] = 'image/avif';
    return $formats;
} );

Since WordPress 6.7 core uses the same mechanism by default for HEIC photos from iPhones: when the server can read them, they are converted to JPEG on upload. Format changes apply only to new or regenerated images, so run wp media regenerate after enabling the filter.

#How to remove TimThumb from an inherited WordPress site

On any theme or plugin dump you did not author:

  1. Search wp-content for timthumb.php, thumb.php, and strings like timthumb.php?src=.
  2. Delete the script after templates no longer call it.
  3. Replace markup:
// Old - do not ship.
echo '<img src="' . esc_url( get_template_directory_uri() . '/timthumb.php?src=' . $url ) . '" alt="" />';

// New - Media Library path.
the_post_thumbnail( 'blog-list' );
  1. Regenerate intermediates, purge CDN cache for old TimThumb URLs, and re-check the homepage LCP image.
  2. Walk archive pages, sidebar widgets, and footers too: those are where old themes tended to hide extra TimThumb calls.

#How to find TimThumb references with grep

Deleting the file is not enough. Find every reference first:

# Theme PHP files
grep -r "timthumb" wp-content/themes/your-theme/
grep -r "thumb.php" wp-content/themes/your-theme/

# Custom plugins as well
grep -r "timthumb" wp-content/plugins/

# The injected-code footprint Maunder told site owners to check
grep -rl "base64_decode" wp-content/

Replace each hit with the_post_thumbnail() or wp_get_attachment_image() and the right size name. A base64_decode hit does not prove an infection, since some legitimate plugins use the function, but every occurrence inside a theme deserves a read. For a structured review, see the WordPress security audit and the hardening guide.

Need a second pair of eyes on an old theme still calling resize scripts? Talk to a WordPress developer who can strip TimThumb, register the sizes your layouts need, and wire CDN transforms without leaving a PHP endpoint on the origin.

#How to measure image performance in WordPress

Compare before and after the migration with PageSpeed Insights, Lighthouse, and WebPageTest. Three numbers tell you whether the switch worked:

  • Largest Contentful Paint (LCP): the hero is usually the LCP element. Pre-generated sizes, fetchpriority="high", and a CDN should bring it down.
  • Cumulative Layout Shift (CLS): every image needs width and height. The attachment helpers add them for you; hand-written TimThumb markup usually did not.
  • Time to First Byte (TTFB) on image requests: without an on-the-fly PHP resize, an image response no longer waits for PHP and GD.

On the server side, give image files long-lived Cache-Control headers (a year is common for versioned uploads), let the CDN handle format conversion, and preload the LCP image only when it is not discoverable early in the HTML.

#TimThumb replacement checklist for WordPress

  • No timthumb.php / thumb.php in themes or mu-plugins.
  • Featured images and content images go through attachment APIs.
  • Size names match real templates; unused sizes removed.
  • srcset present on content images; sizes corrected for tight layouts.
  • On-the-fly needs live on the CDN, not in theme PHP.
  • WebP/AVIF in the delivery path where the stack supports them.

TimThumb taught the ecosystem that “convenient resize in the theme” is a security and ops debt. Core intermediate sizes, srcset, and edge transforms are the maintained stack. Use them and leave the 2011 script in the history books.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

Frequently asked questions

Practical answers to apply the topic in real execution.

Why is TimThumb considered insecure in 2026?#
TimThumb was hit by a critical remote code execution flaw in August 2011: versions before 2.0 only partly matched the source host against the domain whitelist, so attackers could pull a PHP file into the writable cache directory and request it directly (CVE-2011-4106). The script is unmaintained. Core media APIs close that attack surface by writing intermediate files under uploads instead of fetching and rewriting arbitrary URLs through a theme PHP endpoint.
How do I replace TimThumb with core image sizes?#
Delete timthumb.php, register the crops you need with add_image_size, swap template markup to the_post_thumbnail or wp_get_attachment_image, then run wp media regenerate so existing attachments get the new intermediates. Confirm no theme or plugin still builds a /timthumb.php?src= URL.
Do I still need on-the-fly resizing?#
Usually no on the origin. WordPress already stores intermediate sizes and builds srcset. When you need arbitrary dimensions for ads or design systems, push the transform to a CDN that resizes at the edge from a single master file, not a PHP script inside the theme.
What breaks if I delete TimThumb without regenerating?#
Templates that still point at timthumb.php return 404s or broken images. Layouts that relied on hard crops may look wrong until intermediates exist. Regenerate first or in the same deploy window, then remove the script.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles