Skip to content

id(web): /blob serves any blob as text/html via the ?filename= parameter — no CSP/nosniff/disposition headers (same-origin XSS by link) #908

Closed

Description

@dezren39

Problem

GET /blob/{hash} chooses its Content-Type from the caller-supplied ?filename= query parameter and sends no protective headers, so any stored blob can be served as active content in the id web origin — stored/reflected XSS by link.

Evidence (verified on main 86e6f300)

  • Route: pkgs/id/src/web/routes.rs:261 — .route("/blob/{hash}", get(blob_handler)).
  • blob_handler (routes.rs:565-600): the filename comes from query.filename (BlobQuery, routes.rs:~398) when present, falling back to the stored tag name only when the parameter is absent. It then sets Content-Type: get_content_type(&filename) and Cache-Control: public, max-age=31536000, immutable — and nothing else: no Content-Security-Policy, no X-Content-Type-Options, no Content-Disposition.
  • get_content_type (pkgs/id/src/web/content_mode.rs:83-141) maps by extension, including html|htm → text/html; charset=utf-8 (:128), svg → image/svg+xml (:96), js → application/javascript, xml → application/xml.
  • So /blob/<hash of any text file>?filename=evil.html serves those bytes as text/html from the editor's own origin; a top-level visit executes any script in the file with the origin's full API access (save/upload/world admin endpoints are same-origin). Blob hashes are content addresses meant to be shared in this P2P system, so "you need the hash" is not a mitigation. The immutable year-long cache header makes a poisoned URL persistent in the victim's cache.
  • The codebase already knows the safe patterns this handler misses: explore.rs:246-247 sets CSP + X-Content-Type-Options: nosniff on its pages (with tests at explore.rs:759-761), and download_handler (routes.rs:1669-1685) pairs get_content_type with Content-Disposition: attachment.
  • Contributing factor — upload path: upload_handler (routes.rs:1343+) validates only the multipart field's declared Content-Type (:1364-1377) and stores the blob under the field's original file_name() when one is present, so an upload named evil.html declared as image/png is tagged evil.html; the returned URL is built unencoded as format!("/blob/{hash_str}?filename={filename}") (routes.rs:~1495). A script-bearing SVG honestly declared image/svg+xml is likewise stored and later served as image/svg+xml with no sandbox CSP — SVG script runs on top-level navigation.

Why it matters

The web UI is the trusted control surface for the store (edit/save/upload/world administration). One crafted /blob/…?filename=x.html link turns any blob already in the store — including content synced from peers — into same-origin script execution for whoever opens it.

Acceptance criteria

  • blob_handler no longer derives an active Content-Type from the unvalidated ?filename= parameter: either ignore the parameter for typing (use the stored tag name / recorded content-type metadata), or restrict served types to a safe allowlist (images/video/audio/pdf/text-plain) with everything else served as application/octet-stream or Content-Disposition: attachment.
  • Blob responses carry X-Content-Type-Options: nosniff and a restrictive CSP such as Content-Security-Policy: default-src 'none'; sandbox (which still permits <img> embedding of images, the legitimate use — the upload flow inserts the returned URL as an image node src).
  • Regression tests (handler-level, in the style of the existing upload_router_tests in routes.rs): a blob requested with ?filename=x.html is NOT served as text/html; an SVG blob is served with the protective headers; a PNG blob is still served as image/png so embedded images keep working.
  • The upload response URL percent-encodes the filename (as download_handler already does with urlencoding::encode), and an uploaded file's stored name cannot masquerade as a different type than its validated Content-Type (e.g. name extension reconciled with mime_to_extension).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions