Gravity SMTP Leaked Every API Key on the Server — How I Responded to CVE-2026-4020

· 11 min read

A Wordfence threat intel email landed in my inbox on a Tuesday morning in early June. Gravity SMTP — the email plugin from the Gravity Forms team — had a vulnerability under active mass exploitation. Not a theoretical risk buried in a changelog. Seventeen million exploit attempts blocked in six weeks, peaking at over four million in a single day. The payload? Every API key and OAuth token the plugin had ever been configured with, handed to anyone who asked.

I manage north of 70 WordPress sites. Several of them run Gravity Forms with Gravity SMTP handling transactional email through Amazon SES. I had about ten minutes between reading the advisory and being SSH'd into the first server.

What the Vulnerability Actually Does

CVE-2026-4020 is an information disclosure flaw in Gravity SMTP versions up to and including 2.1.4. The plugin registers a REST API endpoint at:

/wp-json/gravitysmtp/v1/tests/mock-data

The problem is in the permission_callback — it unconditionally returns true. No nonce check. No capability check. No authentication of any kind. When you append ?page=gravitysmtp-settings, the plugin's register_connector_data() method populates the internal connector configuration, and the endpoint returns roughly 365 KB of JSON containing the full system report.

That report includes:

  • Live API keys and SMTP credentials for every configured email service (Amazon SES, Google, Mailjet, Resend, Zoho)
  • OAuth tokens for services using OAuth flows
  • PHP version and loaded extensions
  • Web server version and document root path
  • Database server type, version, and table names
  • WordPress version, active plugins with version numbers, and active theme
  • Full wp-config.php constants

In plain English: one unauthenticated GET request dumps your entire server's fingerprint and every email credential the plugin stores. An attacker doesn't need to modify files, create accounts, or leave any trace beyond a single line in your access log.

How I Checked Every Site in Under 30 Minutes

First thing: identify which sites actually have Gravity SMTP installed. I keep a maintenance inventory, but I verified it with WP-CLI across all servers:

wp plugin list --status=active --format=csv --fields=name,version \
  | grep gravitysmtp

On multisite installs, Gravity SMTP may be network-activated rather than per-site. WP-CLI treats that as a separate status, so check for it explicitly:

wp plugin list --status=active-network --format=csv --fields=name,version \
  | grep gravitysmtp

For sites where I don't have WP-CLI set up (a handful of client-managed hosts), I checked via the WordPress admin under Plugins > Installed Plugins or queried the database directly:

SELECT option_value FROM wp_options
WHERE option_name = 'active_plugins'
AND option_value LIKE '%gravitysmtp%';

Out of my 70+ sites, eight had Gravity SMTP active. Three were on version 2.1.3, four on 2.1.4, and one had already been updated to 2.1.5 by the site owner after seeing a dashboard notice. Seven vulnerable.

Checking Access Logs for Exploitation

The critical question: had anyone already hit the endpoint before I got there?

The exploit leaves no conventional indicators of compromise — no modified files, no new admin accounts, no injected code. The only forensic record is in your web server access logs. I searched for requests to the vulnerable endpoint:

grep "gravitysmtp/v1/tests/mock-data" /var/log/nginx/access.log*

Don't forget rotated compressed logs — plain grep won't search .gz files, and if the exploitation happened before your most recent rotation, the evidence is only in the compressed archives:

zgrep "gravitysmtp/v1/tests/mock-data" /var/log/nginx/access.log*.gz

On one of the WHM/cPanel servers running Apache:

grep "gravitysmtp/v1/tests/mock-data" /home/*/access-logs/*
zgrep "gravitysmtp/v1/tests/mock-data" /home/*/access-logs/*.gz

What I was looking for: any GET request to that path, especially with the page=gravitysmtp-settings parameter. Any 200 response confirms the data was retrieved — don't dismiss a hit just because the response body is smaller than expected. Response sizes vary by site configuration, number of connectors, and whether compression is enabled. I've seen confirmed leaks range from under 25 KB to over 380 KB.

grep "gravitysmtp/v1/tests/mock-data" /var/log/nginx/access.log* \
  | awk '{print $1, $4, $9, $10}'

That gives me the IP address, timestamp, HTTP status code, and response size for each matching request.

On two of the seven vulnerable sites, I found hits. Both showed multiple requests from different IP addresses over a three-day window, all returning 200 with response sizes between 350 KB and 380 KB. The credentials had been exposed.

The Fix: Update, Then Rotate Everything

Updating the plugin is the easy part:

wp plugin update gravitysmtp

Gravity SMTP is a licensed plugin distributed through Gravity Forms accounts, not wordpress.org — so wp plugin install won't fetch it from the public repository. If the built-in updater isn't working, download the ZIP from your Gravity Forms account and install it manually:

wp plugin install /path/to/gravitysmtp-2.1.5.zip --force

But updating the plugin doesn't un-leak your credentials. If the endpoint was hit before you patched — or if you can't confirm it wasn't — you have to assume every credential stored in Gravity SMTP has been compromised. Updating without rotating keys is like changing the lock but leaving the stolen key copies in circulation.

Here's what I rotated for the two confirmed-compromised sites:

Amazon SES

Gravity SMTP's Amazon SES integration uses IAM access keys (Access Key ID and Secret Access Key) directly via the SES API — not SMTP credentials. Rotating means creating a new IAM access key for your SES sending user:

  1. Go to IAM > Users > [your SES sending user] > Security credentials
  2. Create a new access key pair
  3. Update the Access Key ID and Secret Access Key in Gravity SMTP's SES connector settings
  4. Send a test email to confirm delivery
  5. Deactivate and then delete the old IAM access key

Do not skip step 6. A leaked access key that's still active is an open door.

Mailjet

  1. Go to Account Settings > REST API > API Key Management
  2. Generate a new Secret Key (the API Key stays the same, but the Secret Key is what authenticates)
  3. Update in Gravity SMTP
  4. Send a test email
  5. Note: Mailjet doesn't let you revoke old secret keys independently — monitor your sending dashboard for unauthorised activity

Google (OAuth)

  1. Go to Google Cloud Console > APIs & Services > Credentials
  2. Revoke the existing OAuth token
  3. Delete the old OAuth client if it was compromised
  4. Create a new OAuth client, re-authorise in Gravity SMTP
  5. Verify sending works

For the Five Sites With No Log Evidence

I updated the plugin immediately and rotated credentials as a precaution on all of them. The absence of evidence in access logs isn't evidence of absence — logs rotate, CDNs can strip paths before they reach origin logs, and some hosting panels don't retain raw access logs at all.

Blocking the Endpoint at the Server Level

Even after patching, I added a server-level block for the vulnerable endpoint. Defence in depth — if the plugin ever regresses or a similar endpoint appears, the server catches it:

WordPress serves REST API routes through two paths: the pretty-permalink /wp-json/ path and the ?rest_route= query parameter fallback. Block both to prevent bypass on sites where pretty permalinks are disabled or where an attacker crafts the request deliberately:

nginx

location ~* /wp-json/gravitysmtp/v1/tests/ {
    deny all;
    return 403;
}

if ($args ~* "rest_route=.*/gravitysmtp/v1/tests/") {
    return 403;
}

Apache (.htaccess)


    RewriteEngine On
    RewriteRule ^wp-json/gravitysmtp/v1/tests/ - [F,L]
    RewriteCond %{QUERY_STRING} rest_route=.*/gravitysmtp/v1/tests/ [NC]
    RewriteRule .* - [F,L]

Reload the web server after adding the rule:

nginx -t && systemctl reload nginx

The Bigger Problem: Plugin Credential Storage

CVE-2026-4020 isn't just a Gravity SMTP problem. It highlights a structural weakness in how WordPress plugins handle credentials.

Most SMTP plugins — Gravity SMTP, WP Mail SMTP, FluentSMTP, Post SMTP — store API keys and SMTP passwords in the wp_options table. Unencrypted. Any plugin vulnerability that exposes wp_options data (SQL injection, information disclosure, insecure REST endpoints) can leak those credentials.

The better approach is to define credentials in wp-config.php using constants and reference them from the plugin. Gravity SMTP supports this:

define( 'GRAVITYSMTP_AMAZON_ACCESS_KEY_ID', 'your-key-here' );
define( 'GRAVITYSMTP_AMAZON_SECRET_ACCESS_KEY', 'your-secret-here' );

This doesn't make the credentials invulnerable — they're still in a PHP file on disk — but it removes them from the database and from any endpoint that serialises wp_options data. It also means credentials don't travel with database exports, staging clones, or migration dumps.

For sites I manage on VPS or dedicated servers, I go a step further and define SMTP credentials in a separate file outside the webroot:

// In wp-config.php
require_once '/etc/wordpress/smtp-credentials.php';
// In /etc/wordpress/smtp-credentials.php (owned by root, readable by www-data)
define( 'GRAVITYSMTP_SES_ACCESS_KEY', 'AKIA...' );
define( 'GRAVITYSMTP_SES_SECRET_KEY', '...' );

That file is outside the document root, not accessible via HTTP, and not included in any backup plugin's scope.

What I Monitor Going Forward

After patching and rotating credentials, I added monitoring for two things:

  1. Access log alerts for REST API test endpoints. Any request matching /wp-json/*/tests/ or /wp-json/*/mock on any managed site triggers a notification via my Uptime Kuma instance. Legitimate test endpoints shouldn't be receiving external traffic.

  2. Plugin version auditing. I already run a weekly WP-CLI sweep across all managed sites to flag outdated plugins. After this incident, I added a specific check for known-vulnerable versions:

wp plugin list --format=csv --fields=name,version \
  | while IFS=',' read -r name version; do
      if [ "$name" = "gravitysmtp" ]; then
        if [ "$(printf '%s\n' "2.1.5" "$version" | sort -V | head -n1)" != "2.1.5" ]; then
          echo "VULNERABLE: $name $version on $(hostname)"
        fi
      fi
    done

The original case pattern I started with only matched 2.1.0–2.1.4 and would have missed any 2.0.x or 1.x installs. Using sort -V for a proper version comparison catches every version below 2.1.5. It runs as part of a daily cron job across every server. A proper vulnerability database integration would be better, and it's on my list.

Timeline

  • 17 March 2026: Gravity SMTP 2.1.5 released with the fix
  • 31 March 2026: CVE-2026-4020 published
  • Early May 2026: First automated exploit attempts detected by Wordfence
  • 22 May 2026: CrowdSec releases detection signatures
  • 27 May 2026: First confirmed in-the-wild exploitation
  • 7 June 2026: Peak exploitation — over 4 million blocked requests in one day
  • Mid-June 2026: 17 million total blocked attempts and counting

Three months between patch and peak exploitation. That window is everything. Sites that update plugins within days of a release were never at risk. Sites on a "we'll update next quarter" cycle had their credentials harvested.

This is exactly the kind of incident that routine maintenance catches before it becomes a crisis. I patch managed sites within 48 hours of any security release. For the sites I manage, seven were vulnerable and two were confirmed hit — but I caught it within hours, not weeks. The credentials were rotated the same day.

If you're running Gravity SMTP, check your version now. If it's below 2.1.5, update immediately and rotate every credential the plugin has access to. If you'd rather not deal with this kind of fire drill yourself, that's what maintenance plans are for — I handle the patching, the log checks, and the credential rotation so you don't have to.

View Maintenance Plans | Get in Touch

Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans Get in Touch

Get in Touch to Discuss Your Needs