Skip to content

Conversation

@orlyjamie
Copy link
Contributor

Summary

SVG files served through the skills and souls file endpoints (/api/v1/skills/:name/file and
/api/v1/souls/:name/file) execute arbitrary JavaScript via <foreignObject>. Because these files are
served from the main clawdhub.com origin with no Content-Security-Policy or Content-Disposition headers,
any script in an SVG runs with full same-origin access to localStorage, including Convex auth JWTs and
refresh tokens.

An attacker can publish a skill containing a malicious SVG, then link to it from their skill's README.
Anyone who clicks the link while logged in has their session tokens exposed. Refresh tokens mean the
attacker maintains access even after the JWT expires.

PoC (open while logged in to ClawdHub): https://clawdhub.com/api/v1/skills/red-pill/file?path=icon.svg

What this patch does

Adds three layers of defense to both file-serving endpoints in convex/httpApiV1.ts:

  • Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src * data:; media-src *
    -- blocks all script execution while still allowing static SVG rendering with inline styles and images
  • X-Content-Type-Options: nosniff -- prevents the browser from reinterpreting content types
  • Content-Disposition: attachment on SVG files specifically -- forces download instead of inline
    rendering as a belt-and-suspenders measure

Affected endpoints

  • skillsGetRouterV1Handler (line ~378)
  • soulsGetRouterV1Handler (line ~987)

Test plan

  • Open the PoC SVG link above after deploying -- should download instead of rendering inline
  • Verify non-SVG files (.js, .md, .json) still serve normally with correct content types
  • Confirm CSP header is present on all file responses via browser dev tools
  • Confirm X-Content-Type-Options: nosniff is present on all file responses

SVG files served via the skills and souls file endpoints can execute
arbitrary JavaScript through <foreignObject>, giving attackers access
to localStorage tokens (including Convex auth JWTs and refresh tokens)
on the same origin.

PoC: https://clawdhub.com/api/v1/skills/red-pill/file?path=icon.svg

This patch adds three layers of defense to both file-serving endpoints:

- Content-Security-Policy: default-src 'none' blocks all script execution
- X-Content-Type-Options: nosniff prevents MIME type sniffing
- Content-Disposition: attachment on SVG files forces download instead
  of inline rendering
@vercel
Copy link
Contributor

vercel bot commented Jan 28, 2026

@orlyjamie is attempting to deploy a commit to the Amantus Machina Team on Vercel.

A member of the Team first needs to authorize it.

@orlyjamie orlyjamie changed the title fix: add CSP headers and Content-Disposition to prevent SVG XSS fix: add CSP headers and Content-Disposition to prevent SVG XSS 🚨 Jan 28, 2026
Copy link

@vignesh07 vignesh07 left a comment

Choose a reason for hiding this comment

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

Solid mitigation for the SVG XSS vector (CSP + nosniff + attachment for SVG). A few nits / suggestions:

  1. CSP: is pretty permissive. If this response is only ever rendered standalone (or downloaded), it may be fine, but consider tightening to (or ) to reduce any chance of abuse if an HTML browser context ever renders it.

  2. : for SVG you might want to include a filename, e.g. (or ) so the download is nice. Optional.

  3. detection: should work for , but if some stored files have missing/incorrect contentType, you might also consider checking the extension as a belt-and-suspenders (e.g. ).

  4. Headers: since you’re returning text, adding could be another cheap layer (though CSP already helps).

CI note: Vercel “Authorization required to deploy” failure looks like an environment permission issue, not code.

Overall: I’m in favor of merging once deploy permissions are sorted.

Copy link

@vignesh07 vignesh07 left a comment

Choose a reason for hiding this comment

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

Solid mitigation for the SVG XSS vector (CSP + nosniff + attachment for SVG). A few nits / suggestions:

  1. CSP: img-src * is pretty permissive. If this response is only ever rendered standalone (or downloaded), it may be fine, but consider tightening to img-src data: (or data: https:) to reduce any chance of abuse if an HTML browser context ever renders it.

  2. Content-Disposition: for SVG you might want to include a filename, e.g. attachment; filename=... (or filename*=UTF-8''...) so the download is nice. Optional.

  3. isSvg detection: contentType?.includes('svg') should work for image/svg+xml, but if some stored files have missing/incorrect contentType, you might also consider checking the extension as a belt-and-suspenders (e.g. .svg).

  4. Headers: since you’re returning text, adding X-Frame-Options: DENY could be another cheap layer (though CSP default-src 'none' already helps).

CI note: Vercel “Authorization required to deploy” failure looks like an environment permission issue, not code.

Overall: I’m in favor of merging once deploy permissions are sorted.

@thewilloftheshadow thewilloftheshadow merged commit c5e5e65 into moltbot:main Jan 29, 2026
1 of 2 checks passed
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