diff --git a/app/d/[slug]/CommentsShell.tsx b/app/d/[slug]/CommentsShell.tsx index fd35a83..0c058bc 100644 --- a/app/d/[slug]/CommentsShell.tsx +++ b/app/d/[slug]/CommentsShell.tsx @@ -803,7 +803,7 @@ export default function CommentsShell(props: Props) { ref={iframeRef} title={title} src={rawSrc} - sandbox="allow-scripts" + sandbox="allow-scripts allow-downloads" referrerPolicy="no-referrer" style={{ border: "none", width: "100%", height: "100%", display: "block", background: "var(--jh-stage-bg, #fff)" }} /> diff --git a/app/d/[slug]/raw/route.ts b/app/d/[slug]/raw/route.ts index 203d6f5..048478c 100644 --- a/app/d/[slug]/raw/route.ts +++ b/app/d/[slug]/raw/route.ts @@ -15,9 +15,10 @@ type Ctx = { params: Promise<{ slug: string }> }; // can never execute same-origin with our auth/session surface (birthday.md "The // one security decision that matters"). // -// Content-Security-Policy: sandbox allow-scripts → the response is treated as -// a unique opaque origin; scripts run but cannot read our cookies / tokens or -// reach our origin's storage. +// Content-Security-Policy: sandbox allow-scripts allow-downloads → the response +// is treated as a unique opaque origin; scripts run and user-initiated file +// downloads work, but the document cannot read our cookies / tokens or reach +// our origin's storage. // X-Content-Type-Options: nosniff → no MIME sniffing. // // Directly linkable for zero-chrome viewing; same token rules as /d/:slug. @@ -97,7 +98,7 @@ export async function GET(req: Request, ctx: Ctx): Promise { status: 200, headers: { "Content-Type": "text/html; charset=utf-8", - "Content-Security-Policy": "sandbox allow-scripts", + "Content-Security-Policy": "sandbox allow-scripts allow-downloads", "X-Content-Type-Options": "nosniff", // Never cache private content at shared caches; tokens are capability URLs. "Cache-Control": "private, no-store",