CVE-2026-35198: Stored XSS in HeyForm Rich-Text Fields — Team Takeover via Silent Payload
| CVE | CVE-2026-35198 |
|---|---|
| GHSA | GHSA-chmm-jqpm-3pwx |
| Severity | Critical CVSS v3.1 base score 9.0 |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N |
| Weaknesses | CWE-79 — Stored Cross-Site Scripting (primary) • CWE-116 — Improper Output Encoding |
| Product | HeyForm (open-source form builder, Node.js/React/TypeScript) |
| Affected | All versions through v3.0.0-rc.6 |
| Fixed in | Commit cc97d27 ("Fix stored XSS in form schema rich text"). No tagged release version explicitly documents this fix. |
| Reported by | Louis Sanchez — Voke Cyber |
| Disclosure | CVE-2026-35198 assigned 2026-04-02. Advisory closed without publishing 2026-05-14. Advisory published 2026-06-22 to enable CVE record publication via GitHub Security Lab. |
Summary
HeyForm's form builder supports rich-text fields that store HTML in the form schema. These fields are rendered via innerHTML / dangerouslySetInnerHTML with no sanitization. Any team member with form-edit access can inject a persistent JavaScript payload that fires automatically in the browser of anyone who renders the form — including the team owner (in the builder), anonymous respondents (on public forms), and team members (in the response dashboard). The payload enables silent session theft and full team ownership takeover. The fix shipped in commit cc97d27 with no public advisory; no Dependabot alert fired; no NVD record exists.
Root cause
HeyForm's form builder supports rich-text elements — formatted text blocks used for welcome screens, section descriptions, and instructional content within forms. These elements accept arbitrary HTML, which is serialized to JSON and stored in the form schema in the database.
At render time, the stored HTML is written directly to the DOM with no sanitization step:
// Raw DOM insertion — no DOMPurify or equivalent element.innerHTML = field.properties.description; // React component — same outcome <div dangerouslySetInnerHTML={{ __html: field.properties.description }} />
Because the value persists in the database, the payload executes on every render of the form — not just at the moment of injection. The three render contexts where it fires are the form builder, the live public form, and the response dashboard. Each reaches a different population of potential victims.
Attack surfaces
1. Form builder (editor)
Any team member opening the form to review, edit, or share it triggers the payload. This is the path to the team owner. Reviewing a form before publishing it is a routine, expected admin action that requires no unusual navigation.
2. Live public form
The published form is accessible to anonymous respondents — anyone with the form URL. No HeyForm account is required. A single injected form can expose thousands of respondents in high-traffic deployments. The payload fires the moment the form loads in the respondent's browser.
3. Response dashboard
Team members reviewing submitted responses may trigger the payload if the form schema is rendered alongside response data. This broadens the internal attack surface beyond the form owner to any team member with response-viewing access.
Attack scenario: team ownership takeover
Prerequisite: A valid team member account with form-edit access. This is the default permission level for all team members — no elevated role or special grant is required.
Step 1 — Inject. The attacker creates or edits a form and inserts an HTML payload into a rich-text block. The payload is designed to execute JavaScript in the context of whoever renders the form next. In the editor, the rich-text block appears as empty space — there is no visual indicator of the injected content.
Step 2 — Wait for a routine action. The team owner opens the form in the builder. This is a normal, expected workflow: reviewing a form before sharing it with the team, checking edits, or preparing to publish. No unusual link, no phishing, no social engineering. The form loads, the rich-text block renders, the payload fires.
Step 3 — Take over. The payload exfiltrates the owner's session credentials to the attacker. With the owner's session, the attacker can:
- Transfer team ownership to an attacker-controlled account
- Export all form responses, including respondent PII (names, emails, survey answers)
- Add the attacker's account with owner privileges
- Modify or delete any form in the team workspace
- Access billing and subscription settings
The entire chain — from payload injection to full account takeover — requires one page load by the owner during a routine task. No interaction or awareness from any victim beyond opening a form they were going to open anyway.
CVSS vector rationale
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N
- AV:N — Network-accessible. The form builder and public form endpoint are reachable over the internet.
- AC:L — No special conditions. Standard form editing, no timing constraints, no race conditions.
- PR:L — Requires a team member account with form-edit access, which is the default for all team members.
- UI:R — A human must render the form. The owner opening the form in the builder satisfies this condition through routine workflow.
- S:C (Scope Changed) — The critical vector. The vulnerability is exploited through the attacker's form-edit privilege, but the impact lands on the team owner's account and all respondent data — a different security scope. This is what elevates the score to 9.0 rather than the 7.x range.
- C:H — Session credentials, respondent PII, and all team data are accessible to the attacker after exploitation.
- I:H — The attacker can modify or delete any form, transfer ownership, and alter billing settings.
- A:N — No direct denial-of-service component; availability impact comes as a consequence of account takeover, not from the exploit itself.
The fix
Commit cc97d27, message "Fix stored XSS in form schema rich text," applies input sanitization to HTML content before DOM insertion, blocking the injection of arbitrary script content through rich-text fields.
Important: no tagged release documents this fix
No release notes, changelog entry, or version bump explicitly attributes this fix to a security vulnerability. Cloud deployments at heyform.net received the commit automatically. Self-hosted operators running a pinned version must check for the commit manually and update if it is absent.
Remediation for self-hosted operators
- Verify commit cc97d27 is present. In your HeyForm deployment directory, run git log --oneline | grep cc97d27. If it is not in the output, your instance is vulnerable.
- Update past that commit immediately. Pull the latest codebase and rebuild. Treat this as a critical security update regardless of whether any other change in the update is needed.
- Do not host public forms on an unpatched instance. Any team member with form-edit access can implant a persistent payload that affects every anonymous respondent filling out any form on the instance.
- Review team membership and form-edit permissions. Until patched, any team member is a potential attack vector. Reduce team membership to the minimum necessary.
- Check for signs of exploitation. Review session logs for unexpected logins from unusual locations. Look for unauthorized team ownership transfers or new owner-level accounts.
The disclosure process and why no CVE record existed until now
This section documents what happened after the fix was shipped, because it is directly relevant to whether self-hosted operators knew to update.
When a security advisory is filed through GitHub's private vulnerability reporting, the maintainer has two paths when the issue is resolved: "Publish advisory" (which creates the public GHSA entry, triggers Dependabot alerts, and pushes the CVE record to cve.org) and "Close" (which dismisses the report as resolved without any public output). In GitHub's UI, both actions close out the open item. Only one of them generates the ecosystem signal that operators depend on.
On May 14, 2026, iMuFeng fixed the issue in commit cc97d27 and closed the advisory thread. No public advisory was created. No Dependabot alert fired. CVE-2026-35198 remained absent from cve.org and NVD.
Follow-up comments on May 14 and May 23 explained this distinction and requested the publish step. Neither received a response. An email to GitHub Security Lab on June 10 confirmed that GitHub cannot force-publish a private GHSA, but can push the CVE record once the researcher publishes a public write-up and provides the URL. This advisory is that write-up.
The maintainer fixed the bug. That part is not in question and deserves credit. The result of the advisory process, however, is that every self-hosted HeyForm operator who has not gone looking for security commits in the git log has had no way to know that a CVSS 9.0 vulnerability existed and was fixed.
Disclosure timeline
- 2026-03-28 — Vulnerability discovered during source-code audit of heyform/heyform. Reproduced on local v3.0.0-rc.6 instance. Full attack chain confirmed.
- 2026-03-30 — GitHub Security Advisory GHSA-chmm-jqpm-3pwx filed.
- 2026-04-02 — GitHub assigned CVE-2026-35198.
- 2026-05-14 — Follow-up comment posted (6.5 weeks after report). Maintainer (iMuFeng) fixed in commit cc97d27 and closed the advisory thread. Advisory not published. CVE not pushed to cve.org.
- 2026-05-23 — Follow-up comment requesting the publish step. No response.
- 2026-06-10 — Email to GitHub Security Lab. Confirmed they can push CVE record upon receipt of a public write-up URL.
- 2026-06-22 — This advisory published. URL provided to GitHub Security Lab to enable CVE record publication on cve.org.
References
- GitHub Security Advisory: GHSA-chmm-jqpm-3pwx
- Fix commit: cc97d27 — "Fix stored XSS in form schema rich text"
- HeyForm repository: github.com/heyform/heyform
- Blog post (story and disclosure narrative): CVE-2026-35198: HeyForm's Stored XSS Was Fixed. Then the Advisory Was Closed Without Being Published.
Found and reported by Louis Sanchez, Founder & Principal Security Consultant at Voke Cyber (OSCP, OSWA, CISSP, CCSK). Prior advisories: CVE-2026-48507 (Snipe-IT), CVE-2026-42318 (GLPI), and GHSA-jxgw-q79v-g84j (Leantime).
We find the bugs tools miss
Stored XSS in form renderers does not show up in a dependency scan. It shows up when someone audits the code that handles user-controlled HTML. That is how we approach web application testing for clients across the Charlotte, NC area and nationwide.
Get a Quote