Skip to content

fix(security): remove dangerous tags and attributes from DOMPurify allow-list (WCH-SI10-001) - #310

Merged
smohite-nice merged 6 commits into
mainfrom
fedramp/CSA-97602/WCH-SI10-001/DOMPurify
Sep 10, 2026
Merged

smohite-nice merged 6 commits into
mainfrom
fedramp/CSA-97602/WCH-SI10-001/DOMPurify

Conversation

@smohite-nice

@smohite-nice smohite-nice commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Removes dangerous tags and attributes that were explicitly re-permitted in the hand-rolled DOMPurify allow-list, restoring the security boundary the sanitizer was intended to provide.

Scope — user-typed input and PrivacyNotice text only.
sanitizeHTML() in this repo is called on two surfaces:

  1. User-typed messagesmessage-middleware.ts:123 (SEND_MESSAGE path), when disableTextInputSanitization is false
  2. PrivacyNotice bodyPrivacyNotice.tsx:58, sanitizing the tenant-configured notice text

Bot and agent messages are out of scope for this PR.
Incoming bot/agent messages flow through RECEIVE_MESSAGE → addMessage() → @cognigy/chat-components, which never calls sanitizeHTML() in this repo. Bot message HTML sanitization lives in @cognigy/chat-components. Tags like style and iframe used by customers to style or embed content in bot messages are not affected by this change and must be addressed separately in that repo.

FedRAMP SI-10 / AC-4 remediation (WCH-SI10-001 — partial):
The previous allowedHtmlTags and allowedHtmlAttributes arrays re-permitted every tag and attribute that DOMPurify excludes by default, weakening sanitization for user-typed input. This PR restores the correct security boundary for the two surfaces listed above.

The dangerous path only opens when disableHtmlInput: false is configured (non-default). With the default disableHtmlInput: true, stripHtmlToInertText() runs after sanitizeHTML() and strips all HTML anyway — but the permissive allow-list is still a defence-in-depth gap.

Tags removed from allowedHtmlTags:
applet, base, body, embed, form, frame, frameset, head, html, iframe, link, meta, noframes, object, style

Note on html / head: these were inconsistently left in the previous commit alongside the removal of body. All three are structural document elements with no legitimate use in sanitised chat fragments; removed for consistency.

Known minor behaviour change: iframe and noframes are members of DOMPurify's DEFAULT_FORBID_CONTENTS. Once removed from ALLOWED_TAGS, they are stripped along with their entire subtree rather than just unwrapped. In practice this only affects developer-facing bots where a user literally types an <iframe> tag — the text content following the tag is also removed. Not a regression in normal use.

Attributes removed from allowedHtmlAttributes:
action, formaction, sandbox, srcdoc, style, target

All other tags (img, audio, video, table, a, b, p, etc.) and safe attributes (href, src, alt, rel, etc.) are preserved — no functional regression for PrivacyNotice content or user text input under non-default configurations.

Jira: CSA-97602

Success criteria

  • None of the removed tags appear in the DOM after user-typed input is submitted containing those tags
  • None of the removed attributes appear on any element after user-input sanitization
  • Bot message rendering is unaffected — customers using style or iframe in bot messages see no change
  • All existing Cypress tests pass — no regression in safe tag rendering

How to test

  1. Run npm run build && npm test — all Cypress tests including the new sanitize.cy.ts suite pass
  2. With disableHtmlInput: false, submit an iframe with a srcdoc script — verify no iframe element appears in chat history
  3. Submit a base tag with an attacker href — verify no base element appears and page URLs are unchanged
  4. Submit a form with an attacker action — verify no form element appears
  5. Receive a bot message with inline styles or an iframe — verify it renders correctly and is unaffected

Security

  • Possible injection vector
  • Authentication/Access controls touched
  • Sensitive Data could be exposed
  • XSS
  • Logging/Monitoring touched
  • Exchanges data with external systems
  • No security implications

Accessibility (WCAG 2.2 AA)

  • No accessibility implications (non-UI change)

Additional considerations

  • This PR might have performance implications

Documentation Considerations

No documentation update required — allowedHtmlTags and allowedHtmlAttributes are internal implementation details. Bot message rendering (and its allow-list) lives in @cognigy/chat-components and is not changed by this PR.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings August 25, 2026 07:40
@smohite-nice
smohite-nice temporarily deployed to pr-preview-310 August 25, 2026 07:40 — with GitHub Actions Inactive
@snyk-io

snyk-io Bot commented Aug 25, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the Webchat HTML sanitization layer by tightening the DOMPurify allow-list, aiming to restore DOMPurify’s intended security boundary by no longer re-permitting tags/attributes that are excluded by secure defaults.

Changes:

  • Removes high-risk tags (e.g., iframe, base, form, object, embed, style, meta, link) from the DOMPurify allow-list used by sanitizeHTML().
  • Removes high-risk attributes (e.g., formaction, srcdoc, style, target, etc.) from the DOMPurify allow-list.
  • Adds a Cypress E2E suite to guard against regressions and verify dangerous tags/attributes don’t survive sanitization.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/webchat/helper/sanitize.ts Updates DOMPurify allow-lists to stop re-permitting tags/attributes excluded by secure defaults.
cypress/e2e/sanitize.cy.ts Adds E2E regression tests for stripping dangerous tags/attributes and preserving safe content.
Suppressed comments (2)

cypress/e2e/sanitize.cy.ts:72

  • Same issue as the <base> test: scoping the assertion to [data-cognigy-webchat-root] can miss <meta> if the browser relocates it (e.g., into head). To ensure the tag is fully stripped from the DOM, assert globally (or at least against head).
		it("strips <meta> — was in allow-list, enables HTTP redirect and CSP bypass", () => {
			typeAndSend('<meta http-equiv="refresh" content="0;url=https://attacker.example.com">');
			cy.get("[data-cognigy-webchat-root]").find("meta").should("not.exist");
		});

cypress/e2e/sanitize.cy.ts:77

  • Same issue as the <base> / <meta> tests: <link> can also end up outside the widget subtree (e.g., moved into head), so asserting only within the webchat root can produce a false negative. Assert globally (or at least against head).
		it("strips <link> — was in allow-list, loads external stylesheets", () => {
			typeAndSend('<link rel="stylesheet" href="https://attacker.example.com/evil.css">');
			cy.get("[data-cognigy-webchat-root]").find("link").should("not.exist");
		});

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/webchat/helper/sanitize.ts Outdated
Comment thread cypress/e2e/sanitize.cy.ts Outdated
Comment thread cypress/e2e/sanitize.cy.ts Outdated
@smohite-nice
smohite-nice temporarily deployed to pr-preview-310 August 25, 2026 07:51 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
@smohite-nice
smohite-nice temporarily deployed to pr-preview-310 August 25, 2026 08:02 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
@smohite-nice
smohite-nice temporarily deployed to pr-preview-310 August 25, 2026 08:29 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
@smohite-nice
smohite-nice temporarily deployed to pr-preview-310 August 25, 2026 08:46 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Aug 25, 2026

@kwinto Dmitrii Ostasevich (kwinto) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three issues with this and 309, by increasing severity:

  1. These PRs should be merged into one.
  2. The reported issue stated in the jira ticket is not closed: this hardens the outgoing path (user messages), the report describes incoming path (bot messages). The incoming path config is in the https://github.com/Cognigy/chat-components/ repo.
  3. If we actually remove this capability from the bot messages in the repo above - we can't and should not prohibit style attribute on the elements. It is used by the customers to customize message stylings. Also I remember some customers had iframe use-case previously.

If this comes as a hard requirement for the FedRAMP, we need to consider a separate, harden build of the Cognigy Webchat.

This is also relevant for the recent PRs which harden and limit the xApps iframe capabilities. We strip them of valid use-cases like geolocation for the sake of audit compliance. This would be another argument towards separate build.

Please raise these concerns with the stakeholders.

@smohite-nice
smohite-nice force-pushed the fedramp/CSA-97602/WCH-SI10-001/DOMPurify branch from afa4f90 to 88c7659 Compare August 31, 2026 06:43
@smohite-nice
smohite-nice temporarily deployed to pr-preview-310 August 31, 2026 06:44 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Aug 31, 2026
@smohite-nice
smohite-nice force-pushed the fedramp/CSA-97602/WCH-SI10-001/DOMPurify branch from 88c7659 to 60aa9ed Compare September 3, 2026 07:12
@smohite-nice
smohite-nice temporarily deployed to pr-preview-310 September 3, 2026 07:12 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Sep 3, 2026
@smohite-nice

Copy link
Copy Markdown
Contributor Author
  1. we create PRs specific to issues, so 2 different PRs
  2. have updated PR description: scope is for user-typed messages & not for bot
    could you please re-review? Dmitrii Ostasevich (@kwinto)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. I checked the customer blast radius carefully and it is much smaller than the diff size suggests.

Bot/Flow-authored HTML is not affected by this PR. Bot and agent message content is rendered by @cognigy/chat-components, which ships its own allowedHtmlTags/allowedHtmlAttributes and its own bundled DOMPurify. So customers whose Flows emit <iframe> video embeds, inline style, target="_blank" links, or class/id for CSS customisation are unaffected. PrivacyNotice.tsx is also safe — it feeds the sanitised text to react-markdown without rehype-raw, so raw HTML never rendered there.

That leaves only the user-typed input path, where the impact is a genuine nit.

Known minor regression, calling it out for the record. iframe and noframes are members of DOMPurify's DEFAULT_FORBID_CONTENTS, so once they leave ALLOWED_TAGS the element and its entire subtree are removed rather than unwrapped. Since <iframe> is a raw-text element, everything after a stray tag becomes its content:

in:  "Can I embed an <iframe> in my page?"
was: "Can I embed an  in my page?"
now: "Can I embed an "

Only reachable when a user literally types those tags, which realistically means developer-facing bots. Not worth blocking on. If you want it gone, pass an explicit FORBID_CONTENTS that excludes iframe/noframes — their inner text is inert once the element is stripped. (For the avoidance of doubt: the equivalent <style> behaviour already exists on main and is not introduced by this PR.)

Two documentation nits:

  • The new comment at the top of the tag list says "always blocked regardless of caller configuration", but line 238 still does { ...config, ALLOWED_TAGS: customAllowedHtmlTags } — a tenant can re-permit every removed tag. #309 is what makes that statement true, so either land #309 first or soften the wording to "blocked by default". The attribute comment is accurate as written, since there is no custom-attribute setting.
  • body is removed in the diff but missing from the PR description, the commit message, and the comment block. Also, html, head and title were left in the list, so the "restore DOMPurify defaults" rationale is applied inconsistently. Worth reconciling the audit trail on an SI-10 PR.

This does not close CSA-97602 on its own — see my earlier comment and my review on #309. The tenant-influenced rendering surface is @cognigy/chat-components, whose allow-list still contains iframe, object, embed, applet, frame, frameset, meta, base, link, style, form, svg, template plus action, formaction, sandbox, srcdoc, style, target, and which applies customAllowedHtmlTags with no deny-list. Please keep the ticket open, or split a follow-up, until the same hardening lands there and the dependency is bumped.

Merges cleanly with #309 (verified with git merge-tree); suggest landing this one first so #309's FORBID_TAGS becomes a pure no-op on the default path.

smohite-nice added a commit that referenced this pull request Sep 10, 2026
…WCH-SI10-001)

Three documentation nits from PR #310 review:

1. Soften comment wording — "always blocked regardless of caller configuration"
   overstated: the custom-tag path (customAllowedHtmlTags) can still override
   the list. Reworded to "blocked by default when no custom tag list is
   configured" and added a note that PR #309 ensures dangerous tags are stripped
   from any tenant-supplied list before it is applied.

2. Document body removal — body was removed from allowedHtmlTags in the
   original commit but was absent from the comment block, PR description, and
   commit message. Added to the comment alongside html/head.

3. Remove html and head from allowedHtmlTags — the "restore DOMPurify defaults"
   rationale was applied inconsistently: body was removed but html and head were
   left in. Both are structural document elements with no legitimate use in
   sanitised chat fragments; removing them makes the list consistent.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@smohite-nice
smohite-nice temporarily deployed to pr-preview-310 September 10, 2026 07:30 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Sep 10, 2026
…low-list (WCH-SI10-001)

SI-10 / AC-4 / FedRAMP Moderate: The hand-rolled ALLOWED_TAGS and ALLOWED_ATTR
lists re-permitted every tag and attribute that DOMPurify excludes by default,
negating the sanitizer entirely.

Tags removed (were explicit XSS / URL-hijacking / exfiltration vectors):
  applet, base, embed, form, frame, frameset, iframe, link, meta,
  noframes, object, style

Attributes removed:
  action, formaction  — form phishing
  sandbox             — attacker control of iframe sandbox policy
  srcdoc              — inline HTML document; direct XSS in iframe
  style               — CSS injection and attribute exfiltration
  target              — navigation hijacking

All other tags (img, audio, video, table, a, b, etc.) and safe attributes
(href, src, alt, rel, etc.) are preserved — no functional regression for
legitimate bot message content.

Tests added in cypress/e2e/sanitize.cy.ts: each removed tag and attribute
is verified to be absent from the DOM after a user message is sent through
the sanitizeHTML path (disableHtmlInput:false, disableTextInputSanitization
defaults to false).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
smohite-nice and others added 5 commits September 10, 2026 13:03
…I10-001)

Collapse multi-line typeAndSend() calls and .find().should() chains that
fit within printWidth (≤89 chars) onto single lines as Prettier requires.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Lines 64 and 90 exceeded printWidth 100 (106 and 101 display chars).
Wrap the it("...", () => { body }) pattern so each line stays within 100.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…H-SI10-001)

Flatten nested describe blocks to a single level (matching disableHtmlInput.cy.ts
pattern) so all it() lines stay within printWidth:100. Shortened test descriptions
to eliminate 2-tab prefix that was pushing lines over the limit.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…WCH-SI10-001)

form test: webchat renders its own <form.webchat-input-menu-form> in the input
area, so find("form") on the whole root always found it. Scoped to
.webchat-chat-history where injected <form> tags would appear.

style-attr test: Emotion CSS-in-JS adds [style] to many webchat elements, so
find("[style]").should("not.exist") always failed. Changed to
find('[style*="attacker.example.com"]') to match the specific injected value.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…WCH-SI10-001)

Three documentation nits from PR #310 review:

1. Soften comment wording — "always blocked regardless of caller configuration"
   overstated: the custom-tag path (customAllowedHtmlTags) can still override
   the list. Reworded to "blocked by default when no custom tag list is
   configured" and added a note that PR #309 ensures dangerous tags are stripped
   from any tenant-supplied list before it is applied.

2. Document body removal — body was removed from allowedHtmlTags in the
   original commit but was absent from the comment block, PR description, and
   commit message. Added to the comment alongside html/head.

3. Remove html and head from allowedHtmlTags — the "restore DOMPurify defaults"
   rationale was applied inconsistently: body was removed but html and head were
   left in. Both are structural document elements with no legitimate use in
   sanitised chat fragments; removing them makes the list consistent.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@smohite-nice
smohite-nice force-pushed the fedramp/CSA-97602/WCH-SI10-001/DOMPurify branch from 7cf9706 to 872dfcf Compare September 10, 2026 07:34
@smohite-nice
smohite-nice temporarily deployed to pr-preview-310 September 10, 2026 07:34 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Sep 10, 2026
@smohite-nice
smohite-nice merged commit db49063 into main Sep 10, 2026
11 checks passed
github-actions Bot added a commit that referenced this pull request Sep 10, 2026
@sushmi21 Sushmitha Sekar (sushmi21) mentioned this pull request Sep 11, 2026
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants