CVE-2026-35198: HeyForm's Stored XSS Was Fixed. Then the Advisory Was Closed Without Being Published.
Here is a scenario that does not get discussed enough in vulnerability disclosure: the vendor fixes the bug, the researcher follows up asking for a public advisory, nobody responds, and the fix ships quietly into the codebase with no signal for anyone running the software. No Dependabot alert. No NVD entry. No changelog callout. Nothing for a self-hosted operator to act on.
That is where CVE-2026-35198 sits right now. A CVSS 9.0 Critical stored XSS in HeyForm was found, reported, and fixed. The advisory was closed without being published. After two follow-up comments and an email to GitHub Security Lab, the CVE record still has not reached cve.org. This article exists partly because GitHub confirmed they can push the CVE record once a public write-up is available — so this is also that write-up.
The short version
HeyForm's rich-text form fields render HTML without sanitization. Any team member with form-edit access can inject a persistent payload that fires silently in the team owner's browser — exfiltrating their session and enabling a full team ownership takeover. The fix is in commit cc97d27, but no public advisory was ever published. If you run a self-hosted HeyForm instance, check whether that commit is present.
What HeyForm is
HeyForm is an open-source form builder available both as a self-hosted deployment and as a cloud service at heyform.net. Organizations use it to build surveys, registration forms, feedback forms, and data collection flows. It supports multi-team workspaces where different groups can manage their own forms and view their own responses. Each form can be published publicly — anyone with the link can fill it out, no HeyForm account required.
That last detail matters a lot for understanding the blast radius of this vulnerability.
What I found
HeyForm's form builder includes rich-text fields — formatted text blocks used for welcome screens, section descriptions, and instructions inside the form. These fields accept and store HTML. When a form is rendered, that stored HTML is written directly to the DOM without sanitization.
The vulnerable pattern:
// Raw DOM assignment — no sanitization
element.innerHTML = field.properties.description;
// React renderer — same result
<div dangerouslySetInnerHTML={{ __html: field.properties.description }} />
No DOMPurify. No allow-list filtering. No encoding. Whatever HTML value is stored in the form schema gets executed as markup in the viewer's browser. And because the value is stored in the database, it fires every time the form is rendered — not just once.
Three places the payload fires
This is what separates a stored XSS from a reflected one. The payload does not require the victim to click a crafted link. It sits in the form definition and executes automatically on any render:
- In the form builder. Anyone who opens the form to edit or review it triggers the payload. This is the path to the team owner — reviewing a form before sharing it is a routine admin action that requires no unusual clicks.
- In the live public form. Any respondent who fills out the published form triggers the payload. They need no HeyForm account. In high-traffic deployments, this surface reaches thousands of anonymous users.
- In the response dashboard. Team members viewing submitted responses may trigger the payload if the form schema is rendered alongside the response data.
The payload is invisible to anyone viewing the form. In a rich-text block it renders as empty space. There is no indication that anything is wrong.
The attack: one page load to team ownership
Here is the full scenario I confirmed against a local v3.0.0-rc.6 instance. The only prerequisite is a valid team member account with form-edit access — which is the default for all team members, requiring no elevated role:
Step 1. The attacker creates or edits a form and inserts a JavaScript payload into a rich-text block. The payload is designed to exfiltrate the session cookie of whoever renders the form. In the editor, the rich-text block appears empty.
Step 2. The team owner opens the form in the builder. This is a routine action — reviewing the form before publishing it, checking it after an edit, or sharing it with the team. The rich-text block renders. The payload fires in the owner's browser. The owner's session cookie is sent to the attacker's server.
Step 3. The attacker authenticates as the team owner using the stolen session. From the owner account they can transfer ownership to themselves, export all form responses including respondent PII, add attacker-controlled accounts with owner privileges, modify or delete any form, and access billing and subscription settings.
No phishing. No social engineering. No unusual behavior from the victim. One page load during normal work.
Why this is CVSS 9.0 Critical
The score reflects something specific about stored XSS that generic tooling often undersells: scope is changed.
In CVSS terms, scope changed (S:C) means the vulnerability's impact extends beyond the component being exploited. Here, the attacker's form-edit permission is the exploited component. The impact lands on the team owner's account, the team members' data, and every respondent who fills out a public form — all of them in a different security context than the attacker's own session. That is what pushes this above the 8.x range for a standard high-impact XSS and puts it at 9.0.
On a publicly accessible HeyForm instance with self-registration enabled, the attacker doesn't even need a pre-existing account. They register, join or create a team, and the attack surface opens immediately.
I reported it. Then I followed up. Twice. Then I emailed GitHub.
- March 28, 2026 Vulnerability discovered during source-code audit of heyform/heyform. Confirmed on a local v3.0.0-rc.6 instance. Full attack chain verified.
- March 30, 2026 GitHub Security Advisory GHSA-chmm-jqpm-3pwx filed through the project's private vulnerability reporting process.
- April 2, 2026 GitHub assigned CVE-2026-35198 to the report.
- May 14, 2026 Follow-up comment posted after 6.5 weeks with no response. The maintainer (iMuFeng) fixed the issue in commit cc97d27 and then closed the advisory thread. On GitHub, "Close" dismisses the advisory report — it does not create a public entry. CVE-2026-35198 was not pushed to cve.org. No release note or changelog entry was added.
- May 23, 2026 Follow-up comment posted to iMuFeng explaining that the advisory needed to be published via the "Publish advisory" step in GitHub's UI to create the public CVE record. No response received.
- June 10, 2026 Email sent to GitHub Security Lab (security-advisories@github.com). GitHub Security Lab confirmed they cannot force-publish a private GHSA, but can push the CVE record if the researcher publishes a public write-up and provides the URL.
- June 22, 2026 This article published. URL provided to GitHub Security Lab to enable CVE record publication.
What actually happened with the vendor
This is worth being precise about, because the story could read as vendor bad-faith when it almost certainly was not.
iMuFeng fixed the bug. That part worked correctly. The problem is a GitHub UI subtlety: when an advisory is ready to publish, the maintainer needs to click "Publish advisory" explicitly. Clicking "Close" instead dismisses the report as handled without creating a public entry. For a maintainer who does not regularly work with GitHub's security advisory system, the distinction is not obvious — both actions close out the open item, but only one of them creates the NVD entry, the Dependabot alert, and the cve.org record that the ecosystem needs.
After two follow-up comments explaining this and requesting the publish step, nothing happened. No response, no action. The net result is the same regardless of intent: a CVSS 9.0 vulnerability is fixed in the codebase, and every self-hosted HeyForm operator who has not checked the git log has no way to know that update is critical.
What self-hosted HeyForm operators should do right now
- Check whether commit cc97d27 is present in your deployment. Run git log --oneline | grep cc97d27 in your HeyForm directory. If the commit is not there, you are running a vulnerable version.
- Update past that commit immediately. Cloud deployments at heyform.net received the fix automatically. Self-hosted instances running a pinned version do not.
- Do not host public forms on an unpatched instance. The public-form attack surface puts anonymous respondents at risk — people who have no relationship with your team and no way to assess their exposure.
- Review team member access. Any team member with form-edit access was, until this fix, capable of implanting a persistent payload affecting all other team members and all respondents.
The silent-fix problem
When a security fix ships without a public advisory, the burden of knowing shifts entirely to the people running the software. They have to notice a commit with a security-relevant message buried in a busy git log. They have to connect that commit to a vulnerability they were never told existed. Most operators are not checking git logs for security commits on a weekly basis — and they should not have to. That is what advisories, Dependabot alerts, and CVE records exist to do.
A fix that is invisible to operators is not a complete fix from a security standpoint. The code is better. The deployed instances are not, unless someone tells their operators they need to update.
For the full technical detail — the vulnerable code pattern, CVSS breakdown, and remediation — see the full advisory.
The bugs that matter take a human
Stored XSS in form builders does not show up in a dependency scan. It shows up when someone reads the rendering code and asks what happens when the stored value contains script content. That is the same work we do testing web applications and APIs for clients across the Charlotte, NC area and nationwide.
Get a Quote