mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
fix(security): let the app frame its own blob previews
A blob: document inherits the policy of the page that minted it, and WebKit applies the inherited frame-ancestors to the blob frame's own parent. With 'none', Safari on macOS and iOS refused the PDF preview iframe and showed an empty box; Chromium does not apply it there and rendered the preview. 'self' keeps every foreign origin out. Refs #1311
This commit is contained in:
1 parent
6800ff2620
commit
e9886ee554
6 files changed
+34
-31
No files matched your search
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
|
||||
"count": 84,
|
||||
"total": 94069,
|
||||
"total": 94064,
|
||||
"entries": {
|
||||
"app/medications/page-client.tsx": 839,
|
||||
"components/charts/mood-chart.tsx": 1166,
|
||||
@@ -86,6 +86,6 @@
|
||||
"lib/validations/inbound-documents.ts": 1018,
|
||||
"lib/withings/sync-sleep.ts": 884,
|
||||
"lib/withings/sync.ts": 933,
|
||||
"proxy.ts": 870
|
||||
"proxy.ts": 865
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
|
||||
"count": 345,
|
||||
"total": 111909,
|
||||
"total": 111902,
|
||||
"entries": {
|
||||
"app/achievements/page.tsx :: AchievementsPage": 206,
|
||||
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275,
|
||||
@@ -180,7 +180,7 @@
|
||||
"components/mood/mood-tag-picker.tsx :: MoodTagPicker": 257,
|
||||
"components/onboarding/confirm-screen.tsx :: ConfirmScreen": 255,
|
||||
"components/onboarding/done-screen.tsx :: DoneScreen": 408,
|
||||
"components/onboarding/getting-started-checklist.tsx :: GettingStartedChecklist": 425,
|
||||
"components/onboarding/getting-started-checklist.tsx :: GettingStartedChecklist": 423,
|
||||
"components/onboarding/tour.tsx :: OnboardingTour": 544,
|
||||
"components/practitioners/practitioner-list.tsx :: PractitionerList": 230,
|
||||
"components/provenance/source-provenance-view.tsx :: SourceProvenanceView": 363,
|
||||
@@ -347,6 +347,6 @@
|
||||
"lib/withings/sync-ecg.ts :: syncUserEcg": 202,
|
||||
"lib/withings/sync-sleep.ts :: syncUserSleep": 414,
|
||||
"lib/withings/sync.ts :: syncUserMeasurements": 337,
|
||||
"proxy.ts :: proxy": 498
|
||||
"proxy.ts :: proxy": 493
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { NextRequest } from "next/server";
|
||||
* - `frame-src` exists and admits precisely 'self' and blob: — nothing else.
|
||||
* - `img-src` admits blob: alongside 'self' and data:.
|
||||
* - The boundaries that make blob: safe stay untouched: `object-src 'none'`,
|
||||
* `frame-ancestors 'none'`, `default-src 'self'`, nonce-bound script-src.
|
||||
* `frame-ancestors 'self'`, `default-src 'self'`, nonce-bound script-src.
|
||||
* - The vault SERVE route keeps its own stricter document CSP unchanged.
|
||||
*
|
||||
* Security rationale (mirrors the comment in proxy.ts): a `blob:` URL can only
|
||||
@@ -73,7 +73,10 @@ describe("proxy.ts blob preview CSP (#787)", () => {
|
||||
const res = proxy(makeRequest("/documents"));
|
||||
const csp = res.headers.get("content-security-policy") ?? "";
|
||||
expect(directive(csp, "object-src")).toEqual(["'none'"]);
|
||||
expect(directive(csp, "frame-ancestors")).toEqual(["'none'"]);
|
||||
// 'self', never 'none': the blob: preview inherits this policy, and
|
||||
// WebKit applies the inherited frame-ancestors to the preview's own
|
||||
// parent, so 'none' blanks every PDF preview in Safari.
|
||||
expect(directive(csp, "frame-ancestors")).toEqual(["'self'"]);
|
||||
expect(directive(csp, "default-src")).toEqual(["'self'"]);
|
||||
expect(csp).toMatch(/script-src 'self' 'nonce-/);
|
||||
});
|
||||
|
||||
@@ -4,8 +4,7 @@ import { NextRequest } from "next/server";
|
||||
/**
|
||||
* The document vault's decrypt-and-serve route is framed same-origin by the
|
||||
* detail preview (`<iframe>` for inline-class PDFs). The proxy's blanket
|
||||
* `X-Frame-Options: DENY` + `frame-ancestors 'none'` would refuse that
|
||||
* embed, and the blanket page CSP overrides whatever the route sets
|
||||
* `X-Frame-Options: DENY` would refuse that embed, and the blanket page CSP overrides whatever the route sets
|
||||
* (middleware headers win) — so the proxy carves out EXACTLY the
|
||||
* `/original` path with a same-origin framing posture and a document CSP
|
||||
* (`default-src 'none'`; deliberately no `sandbox` — Chromium
|
||||
@@ -58,7 +57,7 @@ describe("proxy.ts document serve-route framing carve-out", () => {
|
||||
const res = proxy(makeRequest("/api/documents/inbound"));
|
||||
expect(res.headers.get("x-frame-options")).toBe("DENY");
|
||||
expect(res.headers.get("content-security-policy") ?? "").toContain(
|
||||
"frame-ancestors 'none'",
|
||||
"frame-ancestors 'self'",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -82,7 +81,7 @@ describe("proxy.ts document serve-route framing carve-out", () => {
|
||||
const res = proxy(makeRequest("/documents"));
|
||||
expect(res.headers.get("x-frame-options")).toBe("DENY");
|
||||
expect(res.headers.get("content-security-policy") ?? "").toContain(
|
||||
"frame-ancestors 'none'",
|
||||
"frame-ancestors 'self'",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -103,11 +102,12 @@ describe("proxy.ts share document serve-route framing carve-out", () => {
|
||||
|
||||
it("keeps the DENY + page-CSP posture on the /c/<token> view page itself", () => {
|
||||
const res = proxy(makeRequest(`/c/${shareToken}`));
|
||||
// The clinician view is a PAGE — it must not be framable and keeps the
|
||||
// page CSP; only the blob subresource under it takes the serve posture.
|
||||
// The clinician view is a PAGE — it keeps the page CSP, framable by its
|
||||
// own origin only (see the blob: note in proxy.ts); only the served
|
||||
// document under it takes the serve posture.
|
||||
expect(res.headers.get("x-frame-options")).toBe("DENY");
|
||||
expect(res.headers.get("content-security-policy") ?? "").toContain(
|
||||
"frame-ancestors 'none'",
|
||||
"frame-ancestors 'self'",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user