The Exploit
An unauthenticated attacker can inject arbitrary JavaScript into form submission data by crafting a POST request with a malicious filename in the files parameter. When a site administrator or user views the stored form submission details in WordPress, the payload executes in their browser with full WordPress admin context.
Step 1: Store the payload
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="action"
wpcf7_submit
------Boundary
Content-Disposition: form-data; name="files[]"; filename="<img src=x
Content-Type: text/plain
dummy file content
------Boundary--
The Contact Form 7 Database Addon receives the file upload, extracts the filename via basename($file), and stores it unsanitized in the database without calling sanitize_text_field().
Step 2: Trigger the payload
An administrator navigates to the plugin's form details page in WordPress admin, viewing stored submissions. The vulnerable code in inc/admin-form-details.php echoes the filename directly into an HTML context:
Response excerpt from admin page:
<p><b>File Attachment</b>: <img src=x
The onerror event fires, executing the attacker's JavaScript in the admin's browser session. From there, an attacker can steal the admin's nonce, modify site content, create new admin accounts, or exfiltrate sensitive data.
What the Patch Did
Before
// inc/admin-form-details.php, lines 63–64
echo '<p><b>'.$key_val.'</b>: <a href="'.$cfdb7_dir_url.'/'.$data.'">'
.$data.'</a></p>';
// line 75
echo '<p><b>'.$key_val.'</b>: '. nl2br($arr_str_data) .'</p>';
// line 84
echo '<p><b>'.$key_val.'</b>: '.nl2br($data).'</p>';
// contact-form-cfdb-7.php, line 171
$form_data[$key.'cfdb7_file'] = $file_name;
After
// inc/admin-form-details.php, lines 63–64
echo '<p><b>'.esc_html($key_val).'</b>: <a href="'.esc_url($cfdb7_dir_url.'/'.$data).'">'
.esc_html($data).'</a></p>';
// line 75
echo '<p><b>'.esc_html($key_val).'</b>: '. nl2br($arr_str_data) .'</p>';
// line 84
echo '<p><b>'.esc_html($key_val).'</b>: '.nl2br($data).'</p>';
// contact-form-cfdb-7.php, line 171
$form_data[$key.'cfdb7_file'] = sanitize_text_field($file_name);
The patch adds two output-escaping functions (esc_html() and esc_url()) at the point where user-controlled data is echoed into HTML and URL contexts, and adds input sanitization (sanitize_text_field()) at the point where the filename is first stored. esc_html() converts HTML metacharacters (<, >, ", ', &) into entities, preventing the browser from parsing them as markup. esc_url() applies URL-specific encoding to the href value. sanitize_text_field() strips HTML tags and encodes dangerous characters before storage, reducing the attack surface upstream.
Root Cause
CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting).
The attacker-controlled value enters via the files[] parameter in a multipart POST request to the WordPress AJAX handler. The plugin extracts the filename with basename($file), stores it in $form_data[$key.'cfdb7_file'] without sanitizing, and persists it to the database. Later, when an admin views the form submission details page, the stored filename is retrieved from the database and echoed directly into HTML contexts in inc/admin-form-details.php without output escaping. The trust boundary crossed is the transition from user input → database storage → admin page render; the plugin treats database-stored values as trusted after retrieval, a dangerous assumption when the input was never sanitized at intake.
Why It Works
The load-bearing fix is sanitize_text_field($file_name) in contact-form-cfdb-7.php. Without it, any malicious filename stored in the database will eventually reach an output context. The three output escapes (esc_html() and esc_url()) in admin-form-details.php are defense-in-depth: they ensure that even if a malicious value somehow reaches the admin page—whether from a database, a GET parameter, or legacy code—it cannot be parsed as HTML or JavaScript. Each escape function is context-specific: esc_html() is correct for text content and attribute values containing plain text; esc_url() adds slash-encoding for URL parameters. The input sanitization happens once, early; the output escapes happen multiple times, at every sink. Together, they follow the principle that you cannot trust a single layer of defence.
Hardening Checklist
-
Sanitize all file uploads at intake: Use
sanitize_file_name()(WordPress API) immediately after receiving the filename from$_FILESor multipart data, before storage or any other processing. -
Apply context-aware output escaping at every echo: Use
esc_html()for HTML content,esc_attr()for HTML attribute values,esc_url()for href/src attributes, andwp_kses_post()for rich text. Do not rely on prior sanitization; escaping is the last line of defence. -
Audit the dataflow from user input to output: Trace every variable that flows from
$_POST,$_GET,$_FILES, or database retrieval into anecho,print(), or attribute assignment. Use static analysis tools likephpcswith the WordPress coding standard to catch unescaped echoes. -
Implement a Content Security Policy (CSP) header: Set
Content-Security-Policy: default-src 'self'to prevent inline script execution even if an attacker bypasses output escaping. -
Never assume database values are trusted: A database is a cache of user input. Escape at output time, regardless of whether a value came from GET, POST, a file, or a query result.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-6740
- Contact Form 7 Database Addon GitHub repository (changelog and security advisories)