How BitFire Blocks CVE-2026-96326: HT Contact Form Stored XSS

WordPress vulnerability research

BitFire FREE Bot Protection stops the scripted submission that delivers CVE-2026-96326 and the WAF strips its XSS payload before WordPress runs.

No authentication required CVSS 7.2 High Script execution on entry view Stored Cross-Site Scripting
BitFire · Vulnerability advisoryResearch published
AdvisoryCVE-2026-96326
ComponentHT Contact Form – Drag & Drop Form Builder for WordPress
Relevant sourceadmin/Includes/Api/Endpoints/Submission.php — sanitize_richtext_field()
Executive summary

What WordPress administrators need to know

HT Contact Form – Drag & Drop Form Builder (10,000 active installs) accepts submissions over the unauthenticated REST route /wp-json/ht-form/v1/submission, and its Rich Text field trusts markup that wp_kses() misparses. In 2.10.2, HTML crafted to hide tag and attribute boundaries survives the sanitizer, persists in stored entries, and executes as script in the browser of anyone who views an injected page — a stored cross-site scripting flaw rated CVSS 7.2 with changed scope. BitFire FREE stops both halves of the chain: Bot Protection blocks the scripted submission before WordPress runs, and the WAF discards cross-site scripting payloads from the request body. Version 2.10.3 replaces the sanitizer with a parse-then-enforce DOM allow-list.

At a glance

Key facts

  • The REST route /wp-json/ht-form/v1/submission registers a permission callback that returns true unconditionally, so no WordPress login is required to submit.
  • Rich Text field values reach sanitize_richtext_field(), whose 2.10.2 gate of wp_kses() plus double-quote-only regex rewrites misses markup a browser re-parses into live event-handler attributes.
  • Injected entries persist via Entries::create() and re-execute as HTML in notification email templates and admin entry views (CVSS 7.2, scope changed).
  • BitFire FREE Bot Protection classifies the scripted REST form submission as automated form traffic and blocks it before any vulnerable plugin code runs.
  • BitFire FREE WAF inspects the POST body and discards requests carrying cross-site scripting payloads before persistence.
  • 2.10.3 parses richtext HTML with libxml DOMDocument and enforces a strict tag and attribute allow-list, eliminating event handlers regardless of quoting.
01
Vulnerability overview

Understand the exposure

The affected component, attack path, and practical risk for WordPress websites.

Affected componentHT Contact Form – Drag & Drop Form Builder for WordPress
Potential reach10000 installations
Attack techniquestored cross-site scripting
Published2026-09-29
BitFire FREE breaks CVE-2026-96326 at two independent points: Bot Protection blocks the scripted submission that carries the exploit before WordPress runs, and the WAF strips its cross-site scripting payload from the request body before anything is stored.
02
Technical analysis

How the vulnerability works

Research details, affected versions, exploitation behavior, and remediation guidance.

How CVE-2026-96326 Stores Malicious HTML in Every Entry View

HT Contact Form's public submission route (/wp-json/ht-form/v1/submission, POST) registers a permission callback that returns true unconditionally, so any unauthenticated scripted client can submit field values exactly like a form. A field configured as type richtext passes the attacker's HTML string straight into sanitize_richtext_field(). Version 2.10.2 gates that value with wp_kses() plus regex rewrites that only recognize double-quoted style and class attributes; markup crafted so kses never sees a tag or attribute boundary passes through untouched, while a browser's HTML parser re-associates the same bytes into a live event-handler attribute. The value is stored via Entries::create() and re-emitted as HTML in notification emails and admin entry views — the render-side wp_kses_post() filter shares kses' parsing engine, so the evasion survives output filtering too. Script then executes in the browser of anyone who accesses an injected page.

BitFire FREE Stops the Attack Before WordPress Runs

Both applicable defenses ship in BitFire FREE for eligible non-commercial sites — no upgrade required for this CVE. Because the exploit is delivered as an unauthenticated scripted POST to a form endpoint, BitFire Bot Protection classifies it as automated form traffic and blocks it outright before any vulnerable plugin code executes; scripts and fake browsers also fail BitFire's JavaScript browser verification. Only a client explicitly allowlisted by the site owner or a fully verified real browser gets past that layer. If a crafted request does reach the filter, BitFire's WAF inspects the POST body, including form fields, and discards requests carrying cross-site scripting payloads such as event-handler-bearing markup before the vulnerable sanitizer or the database ever sees them. Delivery blocked at the bot layer, content blocked at the WAF layer: the stored XSS chain never completes.

The 2.10.3 Patch Replaces Guessing with Parsing

The vendor rebuilt sanitize_richtext_field() in admin/Includes/Api/Endpoints/Submission.php. Instead of trusting kses' regex-style tag splitting, 2.10.3 parses submitted HTML with libxml's DOM parser — the same parsing model a browser applies — then walks the resulting tree with the new sanitize_richtext_dom_node() allow-list. Script, style, iframe, object, embed, svg, math, template, noscript, and form tags are dropped with their content, unknown tags are unwrapped, and every attribute not explicitly allowed for its tag is removed, which ends event-handler survival regardless of quoting. Surviving href, target, rel, class, and style values are rebuilt from validated tokens, output is serialized from the sanitized DOM, and hosts without DOMDocument fall back to wp_strip_all_tags(). The shared Draft.php save/resume flow inherits the same hardening. Update to 2.10.3 or later and confirm the HTCONTACTFORM_VERSION constant or plugin header.

If Your Site Was Affected, Investigate for Persistence.

Patching closes the known path; it does not undo a compromise that already succeeded. Every site that ran 2.10.2 or earlier should assume attacker markup could already be sitting in stored entries and notification emails. Patch immediately, then investigate for compromise: audit stored Rich Text entries for script-bearing HTML and review which users viewed injected pages. Run BitFire Threat Hunter for a thorough post-compromise sweep that uncovers backdoor WordPress administrator accounts, hidden database triggers, long-running PHP processes, and droppers that can restore malware or reinfect the site. Remove every persistence mechanism found, rotate administrator and application credentials, and treat a clean-looking entries list as inconclusive — absence of an obvious malicious file is not proof of a clean site.

Put BitFire in Front of Every Form

CVE-2026-96326 is a reminder that sanitizer bugs in popular form plugins become stored XSS on thousands of sites overnight. BitFire FREE stopped this attack at two independent points — Bot Protection blocking the scripted submission before WordPress ran, and the WAF discarding its XSS payload — without needing a CVE-specific virtual patch. Update HT Contact Form to 2.10.3 now, keep BitFire enabled in front of your forms, and run Threat Hunter if you operated an older release. Deploy BitFire today and stop stored XSS before it reaches your database.

03
Source review

Vulnerable and fixed code

The relevant source is located in admin/Includes/Api/Endpoints/Submission.php — sanitize_richtext_field().

BeforeVulnerable behavior
        $content = wp_kses( $content, $allowed_html, [ 'http', 'https', 'mailto', 'tel' ] );

        // Step 4: Sanitize style attributes - only allow safe CSS properties
        $content = preg_replace_callback(
            '/style\s*=\s*"([^"]*)"/i',
AfterCorrected behavior
        $dom = new \DOMDocument();
        libxml_use_internal_errors(true);
        $dom->loadHTML(
            '<?xml encoding="utf-8" ?><div>' . $content . '</div>',
            LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
        );
        libxml_clear_errors();

        $root = $dom->getElementsByTagName('div')->item(0);
        if (!$root) {
            return '';
        }

        self::sanitize_richtext_dom_node($root);
04
Zero-day protection

Protection from the first exploit request

BitFire protects WordPress servers on day zero—before a vulnerability is publicly known and before other vendors have time to develop signatures or patches.

01 · VerifyStop unknown clients

Bot controls and browser verification stop untrusted automated clients before previously unknown exploit code reaches WordPress.

02 · DetectBlock malicious behavior

General WAF protections identify dangerous request behavior and hostile payloads without waiting for a vulnerability-specific signature.

03 · PreventContain attacks at runtime

RASP follows execution inside PHP and prevents unauthorized changes to protected files, accounts, and database content.

BitFire · WordPress protectionZero-day ready
BitFire combines verified-client controls, behavior-based WAF detection, and runtime RASP enforcement to protect WordPress before an exploit has a name, CVE, signature, or vendor patch.
About the author

Cory Marsh

Cory has more than 20 years of internet security experience and is a lead developer on the BitFire project.

Read BitFire security research →
Protect your WordPress website

Add protection before the next exploit arrives.

BitFire combines bot controls, request inspection, malware detection, and runtime protection in one WordPress security platform.

Protect my site free →