CVE-2026-35198: HeyForm's Stored XSS Was Fixed. Then the Advisory Was Closed Without Being Published.

Louis Sanchez June 22, 2026 8 min read

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:

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.

Want the full technical breakdown — CVSS rationale, source references, and remediation steps?
Read the advisory

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.

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

  1. 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.
  2. Update past that commit immediately. Cloud deployments at heyform.net received the fix automatically. Self-hosted instances running a pinned version do not.
  3. 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.
  4. 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