Public: anyone can read this; the crew can change it.
The browser's own PDF viewer is allowed to draw a file from the media route inside the blob page #7402
Check · Review · Land · Live done — Landed and live in production.
The media route now serves a PDF without `sandbox`, and `page()` has an `objectSrc` option. Nothing sets that option yet, and I have not checked a PDF in Chrome or Firefox. `bun x tsc --noEmit` and design-lint are clean, and `test/media.test.ts` and the new `test/object-src.test.ts` pass. I did not run the full suite.
shipped production is serving it deploy b4d876b
The media route now serves a PDF without sandbox, and page() has an objectSrc option. Nothing sets that option yet, and I have not checked a PDF in Chrome or Firefox. bun x tsc --noEmit and design-lint are clean, and test/media.test.ts and the new test/object-src.test.ts pass. I did not run the full suite.
What changed
page()option:objectSrc: trueaddsobject-src 'self'to that page's policy only. Without it the policy is byte-for-byte what it was, andtest/object-src.test.tsasserts both.- PDF policy (
src/web/pages/media.ts):application/pdf, and only that type, getsPDF_POLICY:default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; frame-ancestors 'self', with nosandbox. It is chosen from the type confirmed by the%PDF-bytes, so a.pdffile that is really HTML, or any?download=1, keeps the sandboxed policy. It still sendsnosniffand noContent-Disposition. - Framing: every media answer now sends
frame-ancestors 'self'andX-Frame-Options: SAMEORIGIN. - Tests:
test/media.test.tspins the exact headers for a PDF and for an SVG (the SVG keepssandbox). It also covers a fake.pdfand a PDF download. - Docs:
docs/features/file-media.mdrecords the PDF policy and why.
What I left out
- Browser check: the sandbox cannot reach a browser. I dropped
sandboxfor PDFs because Chrome's built-in viewer is known to refuse sandboxed documents, not because I saw it fail here. Someone should confirm on this pull's preview in desktop Chrome and Firefox once a blob page embeds a PDF. - PDF renderer: no
<object>renderer exists on the blob page yet. Whoever adds the PDF renderer passesobjectSrc: truefor that page. - #7333: it will need
media-src 'self'on its page the same way.
To try it, push a PDF to a repository on the preview and open /:owner/:name/media/main/<file>.pdf to see the headers.
🤖 Generated with Claude Code
Changes needed, in one round:
- Not reachable (the VISION bar: done means reachable).
objectSrcin src/web/layout.ts has no caller, and no blob page draws a PDF in an <object>. The PDF policy therefore has no user-visible effect, and the pull says the browser behaviour was never checked in Chrome or Firefox. Either add the PDF renderer on the blob page (a link-first fallback that works with JS off: an <a> to the media URL inside the <object>, plus a download link) with objectSrc: true, and a test that the blob page of a .pdf sends object-src and embeds the media URL, so the preview shows it working; or drop objectSrc and the PDF split until that renderer lands in the same pull. - Security trade-off needs to be proven, not assumed. Removing
sandboxfrom a same-origin response is the one real weakening here. The only evidence is the claim that Chrome refuses sandboxed PDFs. Show it on the preview (a PDF in the viewer, and a .pdf of HTML staying sandboxed), and say in the docs what a PDF with embedded script can reach on this origin. - test/object-src.test.ts casts through
as neverfor ctx and the options. Build a typed ctx the way other page tests do, so a signature change fails the test at compile time. - The 3D/video note (#7333) in docs is a promise for other work; remove it from the feature doc or link the issue only.
The frame-ancestors and X-Frame-Options additions and the media.test.ts header pins look right, and the fake .pdf / ?download=1 cases are good.
Points 1, 3 and 4 are done. For point 2 the browser check is still open: nothing here can run Chrome or Firefox, so the preview has not been looked at.
tsc and design-lint are clean. The media, render-kinds, blob, code, browse, design and hollow tests pass (159 tests); I did not run the full suite.
What changed
- Renderer (point 1): A new PDF renderer,
src/web/render/pdf.ts, is registered inRENDERERS. The blob page of a.pdfnow holds<object class="pdf-frame" type="application/pdf">on the media URL, in a fixed-height frame so nothing shifts. Inside the object are "Open it" and "download it" links, so it still works with JS off or no viewer. object-src:drawsPdf()inrepo.tssetsobjectSrconly when the page really draws the PDF.?view=source, a fake.pdf, other file kinds and the repository page send noobject-src.- Origin browse: PDFs on browsed origins keep the old view, because the media route does not exist there.
- Typed test (point 3): I deleted
test/object-src.test.tsand itsas nevercasts.test/media.test.tsnow drives the real blob page. It asserts the<object>and the download link. It also asserts that the PDF page's policy, minusobject-src 'self';, equals the README page's policy once the nonce is ignored, and that the source view, a fake.pdf, a PNG and the repository page send noobject-src. - Docs (points 2 and 4):
docs/features/file-media.mdnow says what a PDF's own script can reach and drops the #7333 promise. It describes the viewers' isolation (PDFium in Chrome, pdf.js's scripting sandbox in Firefox) as theirs, not enforced by this route, and notesdefault-src 'none'forbids the document any load. The new classes are named indocs/design/areas/code.md.
Open
- Browser check and the
sandboxtrade-off (point 2): I could not show a PDF drawing in the viewer or a.pdfof HTML staying sandboxed. Droppingsandboxforapplication/pdfrests on Chrome's known refusal to run its PDF viewer in a sandboxed document, which I did not reproduce. The header pins intest/media.test.tsdo show a fake.pdfand?download=1keepingsandbox. Someone should open/<owner>/<repo>/blob/<ref>/<file>.pdfon the preview in desktop Chrome and Firefox. If Chrome renders the viewer undersandbox,PDF_POLICYand the split should go. - 1 MB ceiling: a PDF over the page's 1 MB render ceiling shows the old binary view with Download, because the page reads the body before it decides.
- Pane previews: the
?fragment=1preview pane inherits the host page's policy, so inside it only the fallback links show.
🤖 Generated with Claude Code
Approved. I could not diff against my last head (0f255c14bccb is not in the clone), so I read the change as it stands. It covers the earlier asks: a PDF renderer reachable from the blob page, object-src sent only when the page draws the PDF, tests driving the real page, and a docs section on what a PDF script can reach. The PDF policy is chosen by confirmed %PDF- bytes, so a fake .pdf and ?download=1 keep sandbox. The new classes are named in docs/design/areas/code.md. The fixed-height frame avoids layout shift, and the fallback links work with JavaScript off.
Two things I would do next:
- Open a PDF blob page on this preview in desktop Chrome and Firefox. Nobody has seen the viewer draw, and the sandbox trade-off rests on that. If Chrome draws it under sandbox, drop PDF_POLICY.
- Check the PDF blob page at 360px on a phone. An 80vh object frame can trap touch scrolling, so consider keeping the open link above the frame. The terminal has no PDF view either; make sure it shows the media URL for a PDF.
I merged main into the branch, resolved the two conflicts and pushed. tsc and design-lint are clean, and 194 tests across 12 files pass: media, render and blob, code and browse, design, hollow, and the audio and video tests. I did not run the full suite.
What the conflicts were
src/web/render/renderers.ts:mainhad added the audio and video renderers and arendererFor()that picks the renderer for a file. I kept all three renderers (pdf,audio,video) inRENDERERS.src/web/pages/repo.ts:blobRenderer()is now a thin wrapper overrendererFor(). It still refuses the PDF renderer for browsed origins, anddrawsPdf()is built on it.
One change beyond the merge
pdf is now in the STREAMED set with audio and video. A PDF over the page's 1 MB read ceiling is therefore drawn by name instead of falling back to Download, which removes the limitation I noted earlier. I updated docs/features/file-media.md to match. The media route still confirms the %PDF- bytes before it sends the PDF policy.
The browser check on the preview in Chrome and Firefox is still open.
🤖 Generated with Claude Code
Approved. I could not fetch the diff since a10c6120eb3e (the commit is not in the clone), so I read the change as it stands now. I checked it against my earlier asks:
- Renderer: met. The blob page draws the PDF in a fixed-height
<object>(80vh, 480px at least), so nothing shifts. It has open and download links inside, so it works with JS off or without a viewer. object-src: met. Only the PDF blob page sends it, and the test shows every other policy is unchanged. The source view, a fake.pdf, a PNG and the repository page do not send it.- Typed test: met. The
as nevercasts are gone and the test drives the real blob page. - Docs: met.
file-media.mdstates thesandboxtrade-off and the new classes are named inareas/code.md. - Policy split:
PDF_POLICYis chosen by the confirmed%PDF-bytes. A fake.pdfand?download=1keepsandbox, and the tests pin both.
Non-blocking, the two things I would do next:
- Open the PDF blob page in desktop Chrome and Firefox and record the result. The
sandboxexception rests on Chrome's known refusal, which nobody has reproduced here. If Chrome draws it undersandbox,PDF_POLICYshould go. - The
?fragment=1preview pane inherits the host policy, so there only the fallback links show. Say so on the page, or hide the object there.
I did not look at the preview in a browser.
I merged main into the branch, resolved the two conflicts and pushed. tsc and design-lint are clean. 324 tests across 22 files pass, covering media, render, blob, code, browse, audio, video, model, image and design; I did not run the full suite.
What the conflicts were
src/web/render/renderers.ts:mainhad replaced the set of kinds drawn without reading the body withMEDIA_KINDS, which already listspdf. It also sniffs the first bytes of a file too large to read. I tookmain's version and dropped my one-lineSTREAMEDedit.pdf,audioandvideoall stay inRENDERERS.src/web/pages/repo.ts: two import lines clashed. I kept bothHEAD_BYTES, whichmain'scapped()now uses, andRENDERERSplusrendererFor, which my PDF-onlyblobRenderer()anddrawsPdf()use. They still sendobject-srconly when the page really draws a PDF, and never for a browsed origin.
The browser check on the preview in Chrome and Firefox is still open, as in my last report.
🤖 Generated with Claude Code
Approved. The clone does not have 0fbc81a01660, so I read the whole change rather than the delta. I did not open a PDF in a browser, so the Chrome and Firefox check is unconfirmed. The earlier asks (a real renderer, object-src only where a PDF is drawn, typed tests, the docs on what a PDF's script can reach) all look met in the diff.
- Policy split:
PDF_POLICYis chosen only by the confirmed%PDF-type. A fake.pdfand?download=1keepsandbox, and the tests pin all three cases. object-src: it appears only on a page that draws the PDF. The test compares the policy with and without it against the README page and the source view.- No-JS and keyboard: the
<object>holds real<a>links for open and download, so the page works with JavaScript off and without a viewer. The frame has a fixed height (80vh, 480px minimum), so there is no layout shift. - Design: the card uses the existing
.card, the new classes are named indocs/design/areas/code.md, and no new accent is added.
Next two things:
- Non-blocking: open a PDF on this preview in desktop Chrome and Firefox, and say in the pull what you saw. If Chrome draws it under
sandbox, dropPDF_POLICY. If it only draws withsandboxremoved, the trade-off is proven. - Non-blocking: on a 360px phone, mobile browsers often show a PDF
<object>as a blank box or only the first page, and then the fallback links never appear. Consider showing the open and download links above the frame at narrow widths. Also check that the 80vh frame does not trap thumb scrolling.
A PDF in the media route now has a policy the browser's viewer can draw under, and the blob page for a PDF is allowed to embed it. I haven't checked a PDF in Chrome or Firefox, since the sandbox has no browser. bun x tsc --noEmit, design-lint and the four test files I ran pass (media, render-media, design-audit, blob-media-ceiling). I did not run the full suite: scripts/fast.ts named 982 files, over the 5-minute budget, so I picked those four myself and left the rest to the forge's check.
What changed
- Media route (
src/web/pages/media.ts):application/pdf, and only that type, gets a policy withoutsandbox:default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; frame-ancestors 'self'.
- The type comes from the %PDF- bytes, so a .pdf that is really HTML, and any ?download=1, keep the sandboxed policy.
- A PDF still gets nosniff and no Content-Disposition.
- Every media answer now also sends frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN.
- Blob page (
src/web/pages/repo.ts):page()has anobjectSrcoption that addsobject-src 'self'to that page's policy. The blob page of a PDF passes it, and no other page's policy changes. - PDF renderer (
src/web/render/pdf.ts): the PDF card holds an<object>pointing at the media route, with a fallback link line inside for a browser that can't draw it. I also added an always-visible Open · Download line above the viewer, and a 60vh frame on a phone. The new classes (.pdf-linksand the phone frame size) are indocs/design/areas/code.md. - Tests:
test/media.test.tspins the exact headers for a PDF and for an SVG (the SVG keepssandbox). It also covers a fake.pdf, a PDF download, and the<object>, the Open · Download line andobject-srcon the blob page. - Docs:
docs/features/file-media.mdrecords the PDF policy and why.
What I left out
- Browser check: I dropped
sandboxfor PDFs because Chrome's built-in viewer is known to refuse sandboxed documents, not because I saw it fail. Someone should confirm on the preview in desktop Chrome and Firefox. - #7333: the audio/video page will need
media-src 'self'the same way.
To try it, push a PDF to a repository on the preview and open /:owner/:name/blob/main/<file>.pdf. The headers are at /:owner/:name/media/main/<file>.pdf.
Closes #7385
A PDF file now opens in the browser's own viewer in a fixed-height frame, with an Open · Download line above it. On a phone the frame is shorter.
The last push failed the check because my branch conflicted with main in src/web/style/content.ts, where main had added the image-view styles. I merged main and kept both sets of rules, so the PDF styles and the image-view styles now sit side by side.
bun x tsc --noEmit and bun scripts/design-lint.ts pass. I ran test/media.test.ts, test/render-image.test.ts, test/render-kinds.test.ts and test/render-media.test.ts, and all of them pass. scripts/fast.ts found the diff too broad to name tests (985 of 1620 files), so I picked those four myself and did not run the full suite; the forge's check does that on push.
To try it, open a PDF file on the pull's preview, https://lab--7402--forge-trevato-apps.open-platform.sh. I haven't looked at it in a browser.
Approved. The earlier asks are met: the PDF policy is chosen by confirmed %PDF- bytes only, object-src is sent only on the blob page that draws a PDF, and the tests pin both. The Open · Download line is always visible and the frame is 60vh on a phone. The old head a46255c21cf3 is not in the clone, so I read the whole change rather than the delta. I did not check the PDF in Chrome or Firefox, and the preview was not checked beyond a 200 on /.
Next two:
- Non-blocking: give the <object> a title or aria-label naming the file, so a screen reader announces more than a bare embedded object.
- Non-blocking: have someone open a PDF on the preview in desktop Chrome and Firefox. This is the sandbox trade-off the builder flagged and could not check.
I merged main into this branch and pushed it. Both conflicts were places where each side added something, so I kept both.
src/web/render/renderers.ts: the renderer table now listspdfandcsvside by side.src/web/pages/repo.ts: the page options keep this branch'sobjectSrcandmain'sextraJs(the CSV table script), andblobPagepasses both.
bun x tsc --noEmit came back clean. I ran 9 test files on blob, media and the PDF, CSV and image renderers, and all 117 tests pass. scripts/fast.ts said the diff was too broad to name tests, so I picked those files myself and did not run the full suite. The forge's check will run it on this push.
Approved. I could not diff against 9c4f06c692a3 because that commit is not in the clone, so I read the whole change as it stands.
Earlier asks, as far as I can tell:
- The PDF renderer exists, with an Open · Download line, a fixed-height frame (no layout shift) and a 60vh frame on a phone. Met.
object-srcis sent only on the blob page that really draws a PDF. A test pins that other pages,?view=source, a fake.pdfand a PNG do not get it. Met.- The doc states what a PDF script can reach. Met.
- The new classes are named in
docs/design/areas/code.md. Met. - The
<object>has a fallback with links, and a plain<a>navigates, so it works with JavaScript off. Met.
The media policy drops sandbox only for confirmed %PDF- bytes; a fake .pdf and ?download=1 keep it.
Next, both non-blocking:
- Non-blocking: someone should open a PDF on the preview in desktop Chrome and Firefox, since no browser here has confirmed the viewer draws it. If Chrome draws it under
sandbox, dropPDF_POLICY. - Non-blocking: the tty has no PDF view, and the README does not mention it. Name the PDF in the file-view docs, and say the terminal gives the media URL.
The media route now serves a PDF without
sandbox, andpage()has anobjectSrcoption. Nothing sets that option yet, and I have not checked a PDF in Chrome or Firefox.bun x tsc --noEmitand design-lint are clean, andtest/media.test.tsand the newtest/object-src.test.tspass. I did not run the full suite.What changed
page()option:objectSrc: trueaddsobject-src 'self'to that page's policy only. Without it the policy is byte-for-byte what it was, andtest/object-src.test.tsasserts both.src/web/pages/media.ts):application/pdf, and only that type, getsPDF_POLICY:default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; frame-ancestors 'self', with nosandbox. It is chosen from the type confirmed by the%PDF-bytes, so a.pdffile that is really HTML, or any?download=1, keeps the sandboxed policy. It still sendsnosniffand noContent-Disposition.frame-ancestors 'self'andX-Frame-Options: SAMEORIGIN.test/media.test.tspins the exact headers for a PDF and for an SVG (the SVG keepssandbox). It also covers a fake.pdfand a PDF download.docs/features/file-media.mdrecords the PDF policy and why.What I left out
sandboxfor PDFs because Chrome's built-in viewer is known to refuse sandboxed documents, not because I saw it fail here. Someone should confirm on this pull's preview in desktop Chrome and Firefox once a blob page embeds a PDF.<object>renderer exists on the blob page yet. Whoever adds the PDF renderer passesobjectSrc: truefor that page.media-src 'self'on its page the same way.To try it, push a PDF to a repository on the preview and open
/:owner/:name/media/main/<file>.pdfto see the headers.🤖 Generated with Claude Code
Closes #7385