Closed feature request

Signed asset URLs: let sandboxed viewers on PRIVATE pages read their own page assets

Colby Knox · 2mo ago ·closed by Colby Knox

From AravK's report embedding an interactive Babylon viewer in the README of a private org component page (adom/feeder-reader): inline content renders, but every asset the embed fetches by URL (bundle, .glb, viewer-config.json, board-map SVGs) 404s.

Root cause is by design: publisher-controlled HTML (readme frame, blob previews, component.viewers slots) is served under CSP sandbox without allow-same-origin, so it runs as an opaque origin. Its fetches are anonymous, and a private page's blobs require auth. The invariant is load-bearing (publisher JS must never hold the signed-in viewer's origin authority) and is not up for weakening.

Ask: short-lived signed asset URLs so a sandboxed frame on a private page can read that page's OWN assets and nothing else.

Sketch:

  • When the wiki renders a page view for an authorized user, it mints capability tokens for the page's declared viewer assets (or a per-page, per-view token) with a short TTL.
  • The frame receives them via URL rewriting of the served HTML or a postMessage handshake; assets are fetched as /blob/...?cap=.
  • Tokens are scoped to one page's asset paths, bearer-style, expiring in minutes; no cookie, no origin authority, nothing mintable from inside the frame.
  • Refresh: the parent page re-issues on expiry via postMessage, or the TTL simply covers a viewing session.

Notes:

  • The existing public-asset carve-out (hero + readme images on sealed-source pages, pagePublicAssetPaths) only fits public listings; extending it cannot serve org-private pages without anonymous exposure, hence capability URLs.
  • Interim guidance given to Arav: use the native viewers (trusted, unsandboxed, authenticated) for the model and schematics; self-contain any custom interactivity.

1 Reply

Colby Knox · 2mo ago

Shipped and verified on prod, exactly per the sketch. The sandbox invariant is untouched: frames stay opaque-origin, no cookies, no wiki authority.

The mechanism. While serving a frame document (readme frame, custom viewer slot, shipped SymView/FpView) to an AUTHENTICATED human who can read the page's source, the wiki mints a short-lived, page-scoped, HMAC-signed read token and injects a small bootstrap into the served HTML. The bootstrap wraps fetch and rewrites static img/source/video/audio srcs to append ?cap= to same-host URLs, so EXISTING embeds (the adom/feeder-reader readme) work on private pages with zero changes. The file routes (/api/pages/.../files/, /blob/) accept a valid cap as the read grant for that one page's files.

The contract:

  • Token: "..", charset [0-9A-Za-z._-], TTL 15 minutes (CAP_TTL_S), stateless HMAC (CAP_SIGNING_SECRET, else a generated key persisted server-side).
  • Scope: FILE READS on the one page it was minted for. Nothing else: the page API, writes, and every other route ignore it (verified: cap on the page detail answers the same 404 as anonymous).
  • Mint condition: authenticated + source-readable. Anonymous viewers never get one (public pages need none; private pages never serve them the frame).
  • Nothing is mintable from inside the frame; refresh happens outside it.
  • For custom code: window.__wikiCap holds the token inside injected frames, and GET /api/pages/{owner}/{slug}/cap re-mints ({cap, expires_at, ttl_s}) for parent-page integrations that outlive the TTL.

Verified on prod against the real private board page: cap-only file read 200 with no Authorization header, anonymous without a cap stays the uniform 404, cap presented to the page API stays 404. 28-assert suite at test/security/caps.test.mjs pins the token contract (scope, expiry, tamper), the route acceptance, the mint conditions, the injection (quirks-mode safe), and the reads-only boundary.

For Arav: your feeder-reader README should work as-is when viewed by a signed-in org member; the bundle/glb/config fetches and the board-map tags all get the cap automatically. If your viewer constructs URLs dynamically outside fetch, read window.__wikiCap and append it yourself. Chime in here if any of your four asset types still misses.

Log in to reply.