Repository navigation
Closed
Labels
Description
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 fromquery.filename(BlobQuery,routes.rs:~398) when present, falling back to the stored tag name only when the parameter is absent. It then setsContent-Type: get_content_type(&filename)andCache-Control: public, max-age=31536000, immutable— and nothing else: noContent-Security-Policy, noX-Content-Type-Options, noContent-Disposition.get_content_type(pkgs/id/src/web/content_mode.rs:83-141) maps by extension, includinghtml|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.htmlserves those bytes astext/htmlfrom 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-247sets CSP +X-Content-Type-Options: nosniffon its pages (with tests atexplore.rs:759-761), anddownload_handler(routes.rs:1669-1685) pairsget_content_typewithContent-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 originalfile_name()when one is present, so an upload namedevil.htmldeclared asimage/pngis taggedevil.html; the returned URL is built unencoded asformat!("/blob/{hash_str}?filename={filename}")(routes.rs:~1495). A script-bearing SVG honestly declaredimage/svg+xmlis likewise stored and later served asimage/svg+xmlwith 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_handlerno 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 asapplication/octet-streamorContent-Disposition: attachment. - Blob responses carry
X-Content-Type-Options: nosniffand a restrictive CSP such asContent-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_testsinroutes.rs): a blob requested with?filename=x.htmlis NOT served astext/html; an SVG blob is served with the protective headers; a PNG blob is still served asimage/pngso embedded images keep working. - The upload response URL percent-encodes the filename (as
download_handleralready does withurlencoding::encode), and an uploaded file's stored name cannot masquerade as a different type than its validated Content-Type (e.g. name extension reconciled withmime_to_extension).
Activity
developing-today commentedon Oct 25, 2026
CODE-149
fix(id-web): safe blob content types — no caller-chosen text/html (#908…