mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(documents): drop the dead route-level CSP on the serve path
The proxy's serve-route carve-out owns the response CSP (default-src 'none'; frame-ancestors 'self'; no sandbox — Chromium force-downloads sandboxed PDFs), and middleware headers overwrite route headers, so the route-level sandbox value never reached a client. Remove it and point both the route and its header-matrix test at the proxy pin. Refs: document-vault plan W1-T10 posture reconciliation
This commit is contained in:
1 parent
f9ca7fef79
commit
e9b8995486
2 files changed
+21
-10
No files matched your search
@@ -212,7 +212,7 @@ describe("serving-posture header matrix (Class A inline vs Class B attachment)",
|
||||
["image/webp", "scan.webp"],
|
||||
["image/gif", "anim.gif"],
|
||||
])(
|
||||
"Class A %s → inline, true type, nosniff, CSP sandbox",
|
||||
"Class A %s → inline, true type, nosniff",
|
||||
async (mime, name) => {
|
||||
seed(mime, name);
|
||||
const res = await callGet(makeReq("doc-m"), ctx("doc-m"));
|
||||
@@ -220,7 +220,12 @@ describe("serving-posture header matrix (Class A inline vs Class B attachment)",
|
||||
expect(res.headers.get("Content-Type")).toBe(mime);
|
||||
expect(res.headers.get("Content-Disposition")).toContain("inline");
|
||||
expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff");
|
||||
expect(res.headers.get("Content-Security-Policy")).toBe("sandbox");
|
||||
// The response CSP is owned by the proxy carve-out (`default-src
|
||||
// 'none'; frame-ancestors 'self'`, deliberately no sandbox —
|
||||
// Chromium force-downloads sandboxed PDFs). A route-level value
|
||||
// would be overwritten there, so the route must not set one; see
|
||||
// src/__tests__/proxy-document-serve-framing.test.ts for the pin.
|
||||
expect(res.headers.get("Content-Security-Policy")).toBeNull();
|
||||
expect(res.headers.get("Cache-Control")).toBe("private, no-store");
|
||||
},
|
||||
);
|
||||
@@ -250,7 +255,7 @@ describe("serving-posture header matrix (Class A inline vs Class B attachment)",
|
||||
expect(disposition.startsWith("attachment;")).toBe(true);
|
||||
expect(disposition).toContain(name);
|
||||
expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff");
|
||||
// No render context → no sandbox header needed on a pure download.
|
||||
// CSP is proxy-owned on every serve response; the route sets none.
|
||||
expect(res.headers.get("Content-Security-Policy")).toBeNull();
|
||||
expect(res.headers.get("Cache-Control")).toBe("private, no-store");
|
||||
},
|
||||
|
||||
@@ -12,12 +12,19 @@
|
||||
*
|
||||
* - inline (Class A: PDF/JPEG/PNG/WebP/GIF): true Content-Type,
|
||||
* `Content-Disposition: inline`, `X-Content-Type-Options: nosniff`,
|
||||
* `Cache-Control: private, no-store`, plus `Content-Security-Policy:
|
||||
* sandbox` as defence-in-depth for the PDF viewer context.
|
||||
* `Cache-Control: private, no-store`.
|
||||
* - attachment (Class B: Office/text/TIFF/HEIC/XML/JSON): ALWAYS
|
||||
* `Content-Disposition: attachment` with `Content-Type:
|
||||
* application/octet-stream` + `nosniff` — never inline, regardless of
|
||||
* the stored type, so a misclassified file cannot execute in-origin.
|
||||
*
|
||||
* The response CSP is owned by the proxy, not this route: `src/proxy.ts`
|
||||
* carves out exactly this path with `default-src 'none'; frame-ancestors
|
||||
* 'self'` (middleware headers win over route headers, so anything set here
|
||||
* would be dead weight). Deliberately no `sandbox` directive anywhere —
|
||||
* Chromium force-downloads PDFs framed in sandboxed documents instead of
|
||||
* rendering them, which would break the inline preview.
|
||||
* `src/__tests__/proxy-document-serve-framing.test.ts` pins that posture.
|
||||
*/
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
@@ -197,12 +204,11 @@ export const GET = apiHandler(
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
// Private PHI — never cache in a shared / disk cache.
|
||||
"Cache-Control": "private, no-store",
|
||||
// No Content-Security-Policy here: the proxy's serve-route carve-out
|
||||
// owns it (`default-src 'none'; frame-ancestors 'self'`, no sandbox —
|
||||
// Chromium force-downloads sandboxed PDFs). A route-level CSP would be
|
||||
// silently overwritten by the middleware header pass.
|
||||
};
|
||||
if (servingClass === "inline") {
|
||||
// Defence-in-depth for the in-origin render context (notably the PDF
|
||||
// viewer): no scripts, no forms, no top-navigation from the response.
|
||||
headers["Content-Security-Policy"] = "sandbox";
|
||||
}
|
||||
|
||||
return new NextResponse(bytes as unknown as BodyInit, {
|
||||
status: 200,
|
||||
|
||||
Reference in new issue
Block a user