Skip to content

fix(security): add deny-list to customAllowedHtmlTags in sanitizeHTML (WCH-SI10-002) - #309

Open
smohite-nice wants to merge 5 commits into
mainfrom
fedramp/CSA-97603/WCH-SI10-002/samitize
Open

smohite-nice wants to merge 5 commits into
mainfrom
fedramp/CSA-97603/WCH-SI10-002/samitize

Conversation

@smohite-nice

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

Copy link
Copy Markdown
Contributor

Adds a hard deny-list to sanitizeHTML() so that dangerous HTML tags can never be re-enabled through the customAllowedHtmlTags endpoint configuration, regardless of what a tenant supplies.

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.
Incoming bot/agent messages flow through RECEIVE_MESSAGE → addMessage() → @cognigy/chat-components, which never calls sanitizeHTML() in this repo. Tags like style and iframe that customers use for bot message styling and embedding are handled entirely in @cognigy/chat-components and are unaffected by this change.

The <script> tag that loads webchat.js is unrelated.
Blocking <style> or <script> in sanitizeHTML() has zero effect on how the widget loads or renders. The webchat's own styling uses Emotion CSS-in-JS which injects <style> elements directly — DOMPurify never touches the widget's own markup. The deny-list only strips those tags when a user types them into the chat input.

FedRAMP SI-10 / AC-3 remediation (WCH-SI10-002 — partial):
customAllowedHtmlTags previously replaced ALLOWED_TAGS wholesale with no validation. A tenant could set customAllowedHtmlTags: ["script"] and make every session on that endpoint XSS-vulnerable in a single config change.

Changes:

  • src/webchat/helper/sanitize.ts — adds ALWAYS_BLOCKED_TAGS (exported Set of 12 tags) and filters the tenant-supplied list against it before passing to DOMPurify. The filter is case-insensitive (.toLowerCase()) to prevent SCRIPT/Script bypasses. Also adds FORBID_TAGS: [...ALWAYS_BLOCKED_TAGS] to the base DOMPurify config so the block is unconditional on both the default and custom-tags code paths.

Always-blocked tags (regardless of tenant config):
script, iframe, object, embed, applet, frame, frameset, meta, base, link, style, form

Safe tags in customAllowedHtmlTags (e.g. ["p", "b", "span"]) continue to work unchanged. Behaviour when customAllowedHtmlTags is not set is unchanged.

Jira: CSA-97603

Success criteria

  • customAllowedHtmlTags: ["script"] no longer allows <script> tags through sanitizeHTML
  • All 12 always-blocked tags are filtered regardless of casing in the tenant-supplied array
  • Tenants using customAllowedHtmlTags with safe tags (e.g. ["p", "b", "span"]) are unaffected
  • Bot message rendering is unaffected — customers using style or iframe in bot messages see no change
  • customAllowedHtmlTags not set → existing behaviour unchanged

How to test

  1. Run npm run build && npm test — all Cypress tests including the new customAllowedHtmlTags tests pass
  2. Set customAllowedHtmlTags: ["script"] in endpoint config, send a message containing a script tag — verify no <script> element appears and no script executes
  3. Set customAllowedHtmlTags: ["SCRIPT"] (uppercase) — same result (case-insensitive block)
  4. Set customAllowedHtmlTags: ["p", "b"] — verify those safe tags still work
  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

The customAllowedHtmlTags row in docs/embedding.md has been updated to list the 12 always-blocked tags. 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:29
@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

Adds a case-insensitive deny-list to sanitizeHTML() for tenant-supplied HTML tags, with Cypress coverage.

Changes:

  • Filters dangerous tags from customAllowedHtmlTags.
  • Adds regression tests for blocked tags, casing, and safe tags.

Reviewed changes

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

File Review summary
src/webchat/helper/sanitize.ts The deny-list is not applied when customAllowedHtmlTags is unset, leaving blocked tags allowed through the base configuration. Critical issue.
cypress/e2e/customAllowedHtmlTags.cy.ts The tests exercise react-markdown, so they do not verify that sanitizeHTML() performs the filtering. Moderate issue.
Suppressed comments (6)

cypress/e2e/customAllowedHtmlTags.cy.ts:41

  • cy.its() retries until the requested property exists. Since the expected secure result is that __xss is never created, this assertion can time out rather than verify absence. Assert the property is absent directly with cy.window().should("not.have.property", "__xss").
		cy.window().its("__xss").should("equal", undefined);

cypress/e2e/customAllowedHtmlTags.cy.ts:88

  • This test is named as a safe-tag regression, but it never asserts that p or b survives; it only repeats the non-execution check that the Markdown renderer would pass even if all tags were stripped. Add an assertion on the actual sanitized/rendered output for a safe tag (or test sanitizeHTML directly), so the stated safe-tag compatibility criterion is covered.
	it("still allows safe tags listed in customAllowedHtmlTags through to the deny-list filter", () => {
		// "script" is in the list but blocked; "p" is safe and must survive the filter.
		// The privacy notice renders its text via react-markdown which does not render
		// arbitrary HTML, so we verify no dangerous element was injected rather than
		// asserting the <p> element specifically.

cypress/e2e/customAllowedHtmlTags.cy.ts:105

  • The success criteria require case-insensitive filtering for all 12 blocked tags, but this test exercises casing for only script and iframe. A regression for mixed-case style, base, or form, for example, would still pass. Generate mixed-case variants for every entry in blockedTags and assert each corresponding element is absent.
	it("handles uppercase and mixed-case tag names in deny-list check", () => {
		// Attacker supplies "SCRIPT" or "Script" hoping to bypass toLowerCase() check
		initWithAllowedTags(
			["SCRIPT", "Script", "IFRAME"],
			"<script>window.__xss3 = true</script><iframe></iframe>",

cypress/e2e/customAllowedHtmlTags.cy.ts:20

  • The helper leaves the default home screen enabled, but each caller only clicks the toggle. That opens HomeScreen; it does not call startConversation(), so PrivacyNotice never mounts and sanitizeHTML() is not exercised. Add homeScreen: { enabled: false } here (or start the conversation after opening) so these assertions reach the sanitized surface.
	const initWithAllowedTags = (customAllowedHtmlTags: string[], privacyText: string) =>
		cy.visitWebchat().initMockWebchat({
			settings: {

src/webchat/helper/sanitize.ts:257

  • fetchWebchatConfig() returns raw JSON cast to IWebchatConfig, so the TypeScript type does not protect this runtime boundary. A tenant value such as customAllowedHtmlTags: [null] now throws at tag.toLowerCase(), breaking privacy-notice rendering and message sanitization instead of safely ignoring the malformed entry. Guard the array and retain only string tags before lowercasing.
		const safeTags = customAllowedHtmlTags.filter(
			tag => !ALWAYS_BLOCKED_TAGS.has(tag.toLowerCase()),
		);

src/webchat/helper/sanitize.ts:254

  • The added specs cover arrays (including []) but never omit customAllowedHtmlTags, so the false branch that preserves the default configuration is untested. This is an explicit success criterion and should have a case proving the no-setting behavior remains unchanged.
	let configToUse = config;
	if (customAllowedHtmlTags) {

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

Comment thread cypress/e2e/customAllowedHtmlTags.cy.ts Outdated
Comment thread src/webchat/helper/sanitize.ts
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026

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.

Please add a comment about this black list into embedding doc to the related row.

github-actions Bot added a commit that referenced this pull request Aug 26, 2026
@smohite-nice
smohite-nice force-pushed the fedramp/CSA-97603/WCH-SI10-002/samitize branch from 0082fa7 to 9ffec85 Compare August 26, 2026 11:46
github-actions Bot added a commit that referenced this pull request Aug 26, 2026
@smohite-nice
smohite-nice force-pushed the fedramp/CSA-97603/WCH-SI10-002/samitize branch from 9ffec85 to ca1e1d1 Compare August 31, 2026 06:42
github-actions Bot added a commit that referenced this pull request Aug 31, 2026
@smohite-nice
smohite-nice force-pushed the fedramp/CSA-97603/WCH-SI10-002/samitize branch from ca1e1d1 to eef1ceb Compare September 3, 2026 07:15
github-actions Bot added a commit that referenced this pull request Sep 3, 2026

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.

Thanks for adding the docs row — that addresses my earlier comment. The deny-list itself is well built: case-insensitivity is handled, and using FORBID_TAGS plus a pre-filter is genuine defence-in-depth rather than a single gate (DOMPurify applies FORBID_TAGS over ALLOWED_TAGS, so the ordering is correct). Two things need fixing before merge.


1. (Blocking) Malformed customAllowedHtmlTags now throws — blank widget. sanitize.ts:264-265

customAllowedHtmlTags.filter(tag => !ALWAYS_BLOCKED_TAGS.has(tag.toLowerCase()))

This assumes an array of strings, but the value arrives from remote endpoint JSON / embed-time overrides and is deep-merged with no type validation in config-reducer.ts. I ran each shape against the exact filter logic:

value result
["p", null] TypeError: Cannot read properties of null (reading 'toLowerCase')
["p", 1] / [{}] TypeError: tag.toLowerCase is not a function
"p,br" (string) TypeError: customAllowedHtmlTags.filter is not a function

All four are handled gracefully on main — DOMPurify's addToSet tolerates non-strings and non-arrays, so this is a new failure mode introduced here. Impact: PrivacyNotice.tsx:58 calls sanitizeHTML inside useMemo during render, and there is no ErrorBoundary/componentDidCatch anywhere in src/, so React unmounts the entire widget — blank webchat. On the SEND_MESSAGE path (message-middleware.ts:123) it throws out of dispatch and breaks message sending.

const list = Array.isArray(customAllowedHtmlTags) ? customAllowedHtmlTags : [];
const safeTags = list.filter(
    tag => typeof tag === "string" && !ALWAYS_BLOCKED_TAGS.has(tag.toLowerCase().trim()),
);

(The .trim() also closes the cosmetic " script " gap — harmless today since DOMPurify never matches a tag name containing spaces, but it belongs in the same fix.)

2. (Blocking, docs) FORBID_TAGS changes the default path too. sanitize.ts:251

FORBID_TAGS is added to the base config, so all 12 tags are stripped even when customAllowedHtmlTags is unset — they were previously in allowedHtmlTags. The success criteria say "customAllowedHtmlTags not set → existing behaviour unchanged", which the PR's own test (FORBID_TAGS also blocks form in the default config) contradicts, and the docs/embedding.md edit only documents the block under the customAllowedHtmlTags row. Real blast radius is small, but the description and docs should say what the code does. Landing #310 first makes this a genuine no-op and resolves it cleanly.


Non-blocking:

  • Test assertions are vacuous for 8 of the 12 tags. customAllowedHtmlTags.cy.ts:113-137 only sends script/iframe/form/object markup but loops expect(text).not.to.contain('<' + tag) over all 12 — the assertions for embed, applet, frame, frameset, meta, base, link, style pass regardless of implementation. Please include markup for every tag, and add cases for the malformed shapes above.
  • Reconcile with #310. #310 considers body and noframes dangerous enough to delete from the default allow-list, but they aren't in ALWAYS_BLOCKED_TAGS — so after both land, a tenant can re-enable exactly those two via config. Suggest adding them here.
  • A one-time console.warn listing dropped tags would make support cases diagnosable; silent-drop is correct behaviour but opaque to whoever configured it.
  • Merge order. #309 and #310 auto-merge cleanly (git merge-tree, exit 0) and the result is coherent. Recommend #310 first, then this — otherwise main temporarily ships an allowedHtmlTags constant that contradicts effective behaviour.

On CSA-97603 closure. The PR body notes chat-components is out of scope, and I agree for this diff — but the docs now say these tags are blocked "regardless of this setting", and that isn't true on the surface where tenant-influenced content actually renders. @cognigy/chat-components reads the same config.settings.widgetSettings.customAllowedHtmlTags (via useSanitizesanitizeHTMLWithConfig) and passes it straight to ALLOWED_TAGS with no deny-list, then renders through dangerouslySetInnerHTML. Its own defaults still allow iframe, object, embed, base, meta, link, form, style plus srcdoc, action, formaction, sandbox, style, target, and it deliberately re-emits srcdoc — so adding "script" via config yields <iframe srcdoc="<script>…"> surviving sanitisation. Please either soften the docs wording or, better, filter the setting once in config-reducer so both consumers get the sanitised list — that would genuinely close the ticket in one place.

smohite-nice added a commit that referenced this pull request Sep 10, 2026
Three issues fixed based on reviewer feedback:

1. Add type guard to sanitize.ts filter — Array.isArray() check plus typeof
   string guard plus .trim() prevent TypeError when customAllowedHtmlTags
   contains null, numbers, objects, or is a plain string instead of an array.

2. Filter customAllowedHtmlTags in config-reducer.ts before storing — strips
   dangerous tags and non-string entries at config-load time so both webchat
   (sanitizeHTML) and @cognigy/chat-components (which reads the same store key
   directly) receive the pre-sanitised list. BLOCKED_TAGS is defined inline to
   avoid the sanitize.ts → store → config-reducer circular import.

3. Update docs/embedding.md wording — clarifies that blocked tags are removed
   from the tenant-supplied array before it is applied (accurate for both
   consumers now), and notes that non-string entries are silently ignored.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@smohite-nice

Copy link
Copy Markdown
Contributor Author

Thanks Dmitrii, all three points addressed in commit 94caffb:

Blocking 1 — type guard in sanitize.ts
Added Array.isArray() check before the filter, plus typeof tag === "string" guard and .trim() on each element:

const list = Array.isArray(customAllowedHtmlTags) ? customAllowedHtmlTags : [];
const safeTags = list.filter(
    tag => typeof tag === "string" && !ALWAYS_BLOCKED_TAGS.has(tag.toLowerCase().trim()),
);

Blocking 2 / Advisory — chat-components bypass
Rather than only filtering in sanitizeHTML, the blocked-tags filter is now applied in config-reducer.ts at config-load time (both SET_CONFIG and UPDATE_SETTINGS cases), so the value stored in Redux is already clean. Since @cognigy/chat-components reads customAllowedHtmlTags from the same store key, it receives the pre-sanitised list automatically — the bypass is closed at the source.

BLOCKED_TAGS is defined inline in config-reducer to avoid the circular import (sanitize.ts → store → config-reducer). Both sets are identical; a comment flags that they must stay in sync.

Docs — embedding.md
Replaced "always blocked regardless of this setting" with "stripped from this list before it is applied — they cannot be re-enabled by tenant configuration", and added a note that non-string entries are silently ignored. This is accurate for both webchat and chat-components now that filtering happens in config-reducer.

On FORBID_TAGS and the default path: FORBID_TAGS in the base config is intentional defence-in-depth — if a future change inadvertently adds a dangerous tag back to allowedHtmlTags, DOMPurify still blocks it. The PR description has been updated to make clear this is an explicit hardening of the default path. Happy to coordinate merge order with #310 if you prefer the no-op approach.

github-actions Bot added a commit that referenced this pull request Sep 10, 2026
smohite-nice added a commit that referenced this pull request Sep 10, 2026
Three issues fixed based on reviewer feedback:

1. Add type guard to sanitize.ts filter — Array.isArray() check plus typeof
   string guard plus .trim() prevent TypeError when customAllowedHtmlTags
   contains null, numbers, objects, or is a plain string instead of an array.

2. Filter customAllowedHtmlTags in config-reducer.ts before storing — strips
   dangerous tags and non-string entries at config-load time so both webchat
   (sanitizeHTML) and @cognigy/chat-components (which reads the same store key
   directly) receive the pre-sanitised list. BLOCKED_TAGS is defined inline to
   avoid the sanitize.ts → store → config-reducer circular import.

3. Update docs/embedding.md wording — clarifies that blocked tags are removed
   from the tenant-supplied array before it is applied (accurate for both
   consumers now), and notes that non-string entries are silently ignored.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@smohite-nice
smohite-nice force-pushed the fedramp/CSA-97603/WCH-SI10-002/samitize branch from 94caffb to 6e7d5c7 Compare September 10, 2026 07:11
github-actions Bot added a commit that referenced this pull request Sep 10, 2026
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 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 added a commit that referenced this pull request Sep 10, 2026
…low-list (WCH-SI10-001) (#310)

* fix(security): remove dangerous tags and attributes from DOMPurify allow-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>

* chore: fix prettier — inline chained methods in sanitize.cy.ts (WCH-SI10-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>

* chore: fix prettier — wrap two long it() descriptions in sanitize.cy.ts

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>

* chore: rewrite sanitize.cy.ts with flat structure to fix prettier (WCH-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>

* fix(test): tighten form and style-attr assertions in sanitize.cy.ts (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>

* docs(security): reconcile allow-list audit trail per Dmitrii review (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>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
smohite-nice and others added 5 commits September 11, 2026 10:59
… (WCH-SI10-002)

SI-10 / AC-3 / FedRAMP CRITICAL: customAllowedHtmlTags replaced ALLOWED_TAGS
wholesale with no validation, allowing a tenant to re-enable script, iframe,
object, embed and other dangerous tags for all sessions on that endpoint.

Add ALWAYS_BLOCKED_TAGS — a compile-time Set of 12 tags that are filtered out
of any tenant-supplied customAllowedHtmlTags array before it reaches DOMPurify,
regardless of endpoint configuration:
  script, iframe, object, embed, applet, frame, frameset,
  meta, base, link, style, form

The filter is case-insensitive (.toLowerCase()) to prevent SCRIPT/Script bypasses.
Safe tags in customAllowedHtmlTags are unaffected; existing behaviour for
tenants that do not set customAllowedHtmlTags is unchanged.

Tests added (cypress/e2e/customAllowedHtmlTags.cy.ts):
- script blocked even when explicitly listed in customAllowedHtmlTags
- iframe blocked even when explicitly listed
- all 12 ALWAYS_BLOCKED_TAGS blocked in a single pass
- uppercase/mixed-case tag names rejected by toLowerCase() normalisation
- empty customAllowedHtmlTags ([]) strips all tags as before
- WCAG 2.2 AA compliance check on the privacy-notice surface

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

Address two Copilot review comments:

1. FORBID_TAGS in base DOMPurify config (previously only customAllowedHtmlTags
   path was guarded):
   - Move ALWAYS_BLOCKED_TAGS before config so it can be referenced in FORBID_TAGS
   - Add FORBID_TAGS: [...ALWAYS_BLOCKED_TAGS] to the base config — DOMPurify's
     FORBID_TAGS overrides ALLOWED_TAGS at sanitisation time and is preserved
     through the { ...config, ALLOWED_TAGS: safeTags } spread, making the block
     truly unconditional for both the default and custom-tags code paths
   - Keep the pre-filter on customAllowedHtmlTags as defence-in-depth

2. Rewrite tests to exercise sanitizeHTML() via the SEND_MESSAGE boundary:
   - Tests now type and submit text through the real user-input path
   - Assertions read the sanitized text from the Redux message store
   - This gives a direct observable signal on sanitizeHTML() output rather
     than relying on react-markdown's own HTML filtering (which would have
     made the tests pass even without the fix)
   - Added a dedicated test for the default-config FORBID_TAGS path
     (customAllowedHtmlTags not set)

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

cy.its("__xss").should("equal", undefined) timed out because cy.its()
retries until the property EXISTS on the subject. When script is blocked
and window.__xss is never set, it waits 4 s then fails.

Replace with cy.window().then(win => expect(win.__xss).to.be.undefined)
which uses a synchronous assertion in a .then() callback and correctly
passes when the property is absent.

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

Add a security note to the customAllowedHtmlTags row in embedding.md
listing the 12 tags that are unconditionally blocked regardless of the
tenant-supplied list (script, iframe, object, embed, applet, frame,
frameset, meta, base, link, style, form).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Three issues fixed based on reviewer feedback:

1. Add type guard to sanitize.ts filter — Array.isArray() check plus typeof
   string guard plus .trim() prevent TypeError when customAllowedHtmlTags
   contains null, numbers, objects, or is a plain string instead of an array.

2. Filter customAllowedHtmlTags in config-reducer.ts before storing — strips
   dangerous tags and non-string entries at config-load time so both webchat
   (sanitizeHTML) and @cognigy/chat-components (which reads the same store key
   directly) receive the pre-sanitised list. BLOCKED_TAGS is defined inline to
   avoid the sanitize.ts → store → config-reducer circular import.

3. Update docs/embedding.md wording — clarifies that blocked tags are removed
   from the tenant-supplied array before it is applied (accurate for both
   consumers now), and notes that non-string entries are silently ignored.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
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.

3 participants