Skip to content

fix(frontend): sanitize AI chat markdown before rendering#2229

Merged
topher-lo merged 7 commits intomainfrom
codex/fix-html-injection-vulnerability-in-chat-hwfwbm
Mar 3, 2026
Merged

fix(frontend): sanitize AI chat markdown before rendering#2229
topher-lo merged 7 commits intomainfrom
codex/fix-html-injection-vulnerability-in-chat-hwfwbm

Conversation

@topher-lo
Copy link
Contributor

@topher-lo topher-lo commented Mar 2, 2026

Problem: AI chat markdown could render unsafe HTML/attributes; DOMPurify caused SSR blank output and hydration mismatches. CSP alone doesn’t prevent dangerous protocols or clickable phishing links.

Solution:

  • Sanitize during markdown rendering using rehype-sanitize with a safe schema; apply sanitization before KaTeX to preserve math.
  • Constrain URLs with SAFE_MARKDOWN_LINK_PREFIXES and SAFE_MARKDOWN_IMAGE_PREFIXES via Streamdown.
  • In Response, spread ...props first, then set allowed*Prefixes and rehypePlugins so callers can’t override sanitization.
  • Harden CSP (base-uri 'self', script-src-attr 'none').
  • Remove DOMPurify usage to avoid SSR pitfalls; tests mock ESM rehype plugins in jest.setup.js.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/src/lib/sanitize-markdown.ts">

<violation number="1" location="frontend/src/lib/sanitize-markdown.ts:6">
P1: Sanitizing raw markdown with DOMPurify is insufficient; markdown syntax can still produce unsafe links after rendering. Sanitize the rendered HTML (or enforce markdown URL allow-listing) at render time.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@topher-lo
Copy link
Contributor Author

@cursor review

cursor[bot]

This comment was marked as outdated.

### Motivation
- Prevent HTML/attribute injection in AI-generated chat markdown so user-supplied or LLM-produced HTML cannot render active elements (e.g., `<a>`, `<h1>`, inline `style`) in the chat UI.
- Keep full markdown rendering experience while removing dangerous tags/attributes to avoid breaking most markdown and HTML rendering.
- Strengthen the browser-side policy to reduce inline-attribute/script attack surface.

### Description
- Add a markdown sanitization helper using `DOMPurify` at `frontend/src/lib/sanitize-markdown.ts` and forbid risky attributes (notably `style`) via `FORBID_ATTR`.
- Sanitize AI output before rendering by applying `sanitizeMarkdownContent` to streaming assistant text and persisted assistant text in `Messages` (`frontend/src/components/chat/messages.tsx`).
- Sanitize children passed into the reusable AI `Response` component (`frontend/src/components/ai-elements/response.tsx`) so other AI surfaces inherit the fix.
- Add `dompurify` to frontend dependencies (`frontend/package.json` / `pnpm-lock.yaml`).
- Harden frontend CSP headers by adding `base-uri 'self'` and `script-src-attr 'none'` in `frontend/next.config.mjs` to reduce inline/script attribute risk.

### Testing
- Ran code style/lint checks: `pnpm --dir frontend exec biome check --write src/components/ai-elements/response.tsx src/components/chat/messages.tsx src/lib/sanitize-markdown.ts` (succeeded).
- Ran workspace checks: `pnpm -C frontend check` (succeeded).
- Ran TypeScript typecheck: `pnpm -C frontend run typecheck` (succeeded, `tsc` returned no errors).
- Installed `dompurify` via `pnpm --dir frontend add dompurify@3.2.6` (succeeded) and updated lockfile.
- Attempted local visual validation by running `pnpm -C frontend dev --port 3000` and capturing a Playwright screenshot; Next server started and a browser screenshot artifact was produced, but request rendering returned `500` due to missing required environment variables (`NEXT_PUBLIC_APP_URL`, `NEXT_PUBLIC_API_URL`) in this execution environment, so full end-to-end manual verification could not be completed here.
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/src/components/ai-elements/response.tsx">

<violation number="1" location="frontend/src/components/ai-elements/response.tsx:21">
P1: The new safe link/image prefix restrictions are overrideable because `{...props}` is spread after them, allowing callers to bypass the intended markdown URL policy.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

### Motivation
- Prevent HTML/attribute injection in AI-generated chat markdown so user-supplied or LLM-produced HTML cannot render active elements (e.g., `<a>`, `<h1>`, inline `style`) in the chat UI.
- Keep full markdown rendering experience while removing dangerous tags/attributes to avoid breaking most markdown and HTML rendering.
- Strengthen the browser-side policy to reduce inline-attribute/script attack surface.

### Description
- Add a markdown sanitization helper using `DOMPurify` at `frontend/src/lib/sanitize-markdown.ts` and forbid risky attributes (notably `style`) via `FORBID_ATTR`.
- Sanitize AI output before rendering by applying `sanitizeMarkdownContent` to streaming assistant text and persisted assistant text in `Messages` (`frontend/src/components/chat/messages.tsx`).
- Sanitize children passed into the reusable AI `Response` component (`frontend/src/components/ai-elements/response.tsx`) so other AI surfaces inherit the fix.
- Add `dompurify` to frontend dependencies (`frontend/package.json` / `pnpm-lock.yaml`).
- Harden frontend CSP headers by adding `base-uri 'self'` and `script-src-attr 'none'` in `frontend/next.config.mjs` to reduce inline/script attribute risk.

### Testing
- Ran code style/lint checks: `pnpm --dir frontend exec biome check --write src/components/ai-elements/response.tsx src/components/chat/messages.tsx src/lib/sanitize-markdown.ts` (succeeded).
- Ran workspace checks: `pnpm -C frontend check` (succeeded).
- Ran TypeScript typecheck: `pnpm -C frontend run typecheck` (succeeded, `tsc` returned no errors).
- Installed `dompurify` via `pnpm --dir frontend add dompurify@3.2.6` (succeeded) and updated lockfile.
- Attempted local visual validation by running `pnpm -C frontend dev --port 3000` and capturing a Playwright screenshot; Next server started and a browser screenshot artifact was produced, but request rendering returned `500` due to missing required environment variables (`NEXT_PUBLIC_APP_URL`, `NEXT_PUBLIC_API_URL`) in this execution environment, so full end-to-end manual verification could not be completed here.
@topher-lo
Copy link
Contributor Author

@cursor push fc97b08

@cursor
Copy link

cursor bot commented Mar 3, 2026

Could not push Autofix changes. The PR branch has conflicting changes.

@blacksmith-sh

This comment has been minimized.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/src/components/ai-elements/response.tsx">

<violation number="1" location="frontend/src/components/ai-elements/response.tsx:27">
P1: `rehypePlugins` can be overridden by `...props`, allowing sanitization to be bypassed. Apply `...props` first and enforce the sanitizing `rehypePlugins` afterward.</violation>
</file>

<file name="frontend/src/lib/sanitize-markdown.ts">

<violation number="1" location="frontend/src/lib/sanitize-markdown.ts:36">
P2: `rehype-sanitize` is applied after `rehype-katex`, which can strip KaTeX output and break math rendering. Sanitize untrusted markdown first, then run `rehype-katex`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@cursor
Copy link

cursor bot commented Mar 3, 2026

  • I validated both issues (identified by cubic) and fixed them.

    • In frontend/src/components/ai-elements/response.tsx, I now spread ...props first and then set allowedImagePrefixes, allowedLinkPrefixes, and rehypePlugins afterward so callers cannot override/bypass sanitization.
    • In frontend/src/lib/sanitize-markdown.ts, I reordered the rehype plugins to sanitize first and then run KaTeX: [[rehypeSanitize, sanitizeSchema], rehypeKatex]. This avoids KaTeX markup being stripped while still sanitizing untrusted content.
  • I confirmed we no longer use DOMPurify (so the SSR empty-string issue is moot) and ran project checks:

    • pnpm -C frontend check passed
    • pnpm -C frontend run typecheck passed

I committed and pushed: fix(frontend): enforce sanitizing rehypePlugins and correct plugin order.

Open in Web Open in Cursor 

cursoragent and others added 4 commits March 3, 2026 04:18
Co-authored-by: Chris Lo <topher-lo@users.noreply.github.com>
Co-authored-by: Chris Lo <topher-lo@users.noreply.github.com>
Co-authored-by: Chris Lo <topher-lo@users.noreply.github.com>
…anges

Co-authored-by: Chris Lo <topher-lo@users.noreply.github.com>
@topher-lo topher-lo merged commit bf14937 into main Mar 3, 2026
12 checks passed
@topher-lo topher-lo deleted the codex/fix-html-injection-vulnerability-in-chat-hwfwbm branch March 3, 2026 04:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants