Skip to content

fix(files): serve inline SVG previews with a script-blocking CSP - #520

Open
hobostay wants to merge 1 commit into
agegr:mainfrom
hobostay:fix/inline-svg-content-security-headers
Open

fix(files): serve inline SVG previews with a script-blocking CSP#520
hobostay wants to merge 1 commit into
agegr:mainfrom
hobostay:fix/inline-svg-content-security-headers

Conversation

@hobostay

Copy link
Copy Markdown

Problem

GET /api/files/<path>.svg?type=read serves image/svg+xml inline through streamFile(), which sets only Content-Type / Cache-Control / Accept-Ranges / Content-Disposition — no Content-Security-Policy, no X-Content-Type-Options. The DOCX preview route (?type=preview) already ships a restrictive CSP, but SVG is the one repo-controlled preview type a browser executes as a document, and it was left open.

SVG is HTML-capable, so an SVG inside a project opened in Pi Web can carry <script>:

<svg xmlns="http://www.w3.org/2000/svg">
  <script>fetch('/api/auth/api-key/anthropic', {method:'POST', ...})</script>
</svg>

Navigating to the file URL renders it as a same-origin document (http://127.0.0.1:30141/api/files/...), where the script runs with full access to every /api/* route — including routes that read/write provider credentials — and with the user's Basic Auth credentials attached automatically when PI_WEB_PASSWORD is set. A realistic entry point: the agent quotes or emits a link to an SVG asset in a transcript, and MarkdownBody renders non-local hrefs as target="_blank" links; one click executes. This matters precisely for the "open a repo you don't fully trust" workflow the rest of the codebase (path-security, request-security, DOCX CSP) is already hardened against.

Fix

  • Serve image/svg+xml with the same policy the DOCX preview already uses (default-src 'none'; img-src data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'self' + Referrer-Policy: no-referrer), so scripts can never run while legit SVGs keep inline styles and data-URI images when viewed as a document.
  • Add X-Content-Type-Options: nosniff to every streamFile() response.
  • In-app previews are unaffected: CSP response headers are ignored for <img> embedding, so the file viewer renders SVGs exactly as before.

Tests: app/api/files/stream-route.test.mjs (source-assertion, following the existing watch-route.test.mjs convention) asserts the headers exist on the shared header object used by all streamFile responses. Full suite 590/590, tsc --noEmit and eslint clean.

🤖 Generated with Claude Code

…ed as a document

streamFile() served image/svg+xml inline with no Content-Security-Policy
or X-Content-Type-Options, while every other document the app generates
(the DOCX preview) already ships a restrictive policy. SVG is the only
inline preview type a browser executes as a document, so a repo-controlled
SVG opened directly — for example by following a transcript link such as
http://127.0.0.1:30141/api/files/.../logo.svg?type=read — runs script in
the Pi Web origin, with access to every /api route (and the user's Basic
Auth credentials when PI_WEB_PASSWORD is set).

Serve SVG with the same policy the DOCX preview uses, mirroring its
directives so legit SVGs keep inline styles and data-URI images, and add
nosniff to all streamed responses. CSP response headers do not affect
<img> embedding, so in-app previews render exactly as before.

Co-Authored-By: Claude <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.

1 participant