Skip to content

fix: escape admin dashboards and sandbox active static content (stored XSS) - #176

Open
9xj89gzrtw-hue wants to merge 4 commits into
tscircuit:mainfrom
9xj89gzrtw-hue:fix/admin-xss-and-static-sandbox
Open

9xj89gzrtw-hue wants to merge 4 commits into
tscircuit:mainfrom
9xj89gzrtw-hue:fix/admin-xss-and-static-sandbox

Conversation

@9xj89gzrtw-hue

Copy link
Copy Markdown

Fixes #175

Vulnerability chain

POST /files/upsert is unauthenticated, so anything stored must be rendered as untrusted. Before this change:

  1. Admin dashboards rendered user-written strings unescaped: ${file.file_path} (list + details, including <title>), ${file.file_id}, ${file.created_at}, ${event_type} and the JSONified event payload containing the user-supplied initiator. A file written with path /<script>alert(1)</script>/poc.html executed for every visitor of /admin pages (stored XSS).
  2. /files/static served stored content with text/html, image/svg+xml, text/javascript, application/xml on the same origin as /admin — script execution one navigation away from the dashboards.

Fix

  • New lib/utils/escape-html.ts (escapeHtml()), applied to every user-controlled interpolation in admin/files/list, admin/files/get and admin/events/list. Display strings escaped; href attribute values escaped as well, preserving the exact URL text for normal paths so existing consumers (and the existing admin page test) keep working.
  • /files/static: X-Content-Type-Options: nosniff on every response and Content-Security-Policy: sandbox on active content types (text/html, text/javascript, image/svg+xml, application/xml) — including proxied responses re-wrapped dynamically. Body bytes and content types are otherwise unchanged.

Tests

  • tests/routes/admin-xss-escape.test.ts: scripted file_path rendered only as entities on list + details; scripted initiator escaped on the events page; normal file data renders unchanged.
  • tests/routes/static-active-content-headers.test.ts: .html/.svg/.js responses carry sandbox CSP + nosniff; .txt gets nosniff only; body bytes unchanged in all cases.

Full suite 49/49 pass; tsc clean; bun run format:check exit 0.

normalizePath collapsed only the first repeated slash group (missing /g flag),
so paths like a///b///c.txt normalized to a/b///c.txt. The db-client
normalizes file paths on write and again on lookup; a non-idempotent
normalizer can make those comparisons inconsistent.

- replace(/\/\/+/g, "/") now collapses every group
- new tests: multi-group collapse, idempotence, windows separators
and mixed separators, preprocessing edge cases
…scircuit#173)

resolveFileProxy resolved user-controlled segments with join(diskPath,
relativePath) without checking for .. segments, so a request like
/files/download/local/../secret.txt read files outside the proxy root
(CWE-22); the http flavor forwarded the same traversal upstream.

- resolveFileProxy rejects any relative path containing a .. segment (400)
- resolveDiskProxy additionally require the resolved path to stay inside
  resolve(diskPath), independent of platform separator handling
- unit + route-level regression tests using raw HTTP (clients normalize
  dot-dot segments)
POST /files/upsert is unauthenticated, so anything it stores must be
treated as user content everywhere it is rendered. Admin dashboards
rendered file_path/event payloads unescaped (stored XSS on the same
origin as /admin), and /files/static served user HTML/SVG/JS with
script-executing content types on that origin.

- new escapeHtml() util; applied to admin files list, file details
  (display strings; href attribute values escaped without changing
  normal-path URLs) and events list (type + details JSON)
- /files/static sets X-Content-Type-Options: nosniff on every response
  and Content-Security-Policy: sandbox on active content types
  (text/html, text/javascript, image/svg+xml, application/xml),
  including proxied responses; bytes unchanged
- route-level regression tests (49/49 pass full suite)
@9xj89gzrtw-hue

Copy link
Copy Markdown
Author

/claim #175

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.

Stored XSS in admin dashboards + active content served on the admin origin via unauthenticated /files/upsert

1 participant