Voke Cyber Security Advisory

CVE-2026-35198: Stored XSS in HeyForm Rich-Text Fields — Team Takeover via Silent Payload

Louis Sanchez Published June 22, 2026 10 min read
CVECVE-2026-35198
GHSAGHSA-chmm-jqpm-3pwx
SeverityCritical  CVSS v3.1 base score 9.0
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N
WeaknessesCWE-79 — Stored Cross-Site Scripting (primary) • CWE-116 — Improper Output Encoding
ProductHeyForm (open-source form builder, Node.js/React/TypeScript)
AffectedAll versions through v3.0.0-rc.6
Fixed inCommit cc97d27 ("Fix stored XSS in form schema rich text"). No tagged release version explicitly documents this fix.
Reported byLouis Sanchez — Voke Cyber
DisclosureCVE-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:

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

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

  1. 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.
  2. 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.
  3. 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.
  4. Review team membership and form-edit permissions. Until patched, any team member is a potential attack vector. Reduce team membership to the minimum necessary.
  5. 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

References

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