@teamscala/service-boot 0.23.400 → 0.23.401
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/security-headers.test.ts +39 -0
- package/src/security-headers.ts +19 -1
package/package.json
CHANGED
|
@@ -80,3 +80,42 @@ describe("injectSecurityHeaders (fallback mode, no registry)", () => {
|
|
|
80
80
|
expect((res.headers.get("Content-Security-Policy") ?? "")).not.toContain("{{nonce}}");
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
|
+
|
|
84
|
+
describe("Cross-Origin-Resource-Policy — public assets stay embeddable", () => {
|
|
85
|
+
function appWithAssetRoute(): Hono {
|
|
86
|
+
const app = new Hono();
|
|
87
|
+
app.use("*", injectSecurityHeaders);
|
|
88
|
+
app.get("/api/assets/*", (c) => c.body("png-bytes", 200, { "Content-Type": "image/png" }));
|
|
89
|
+
app.get("/", (c) => c.text("ok"));
|
|
90
|
+
return app;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
it("serves cross-origin CORP for /api/assets/* so an email can render the logo", async () => {
|
|
94
|
+
clearRegistryHeaders();
|
|
95
|
+
const res = await appWithAssetRoute().request("/api/assets/org/logos/launcha.png");
|
|
96
|
+
// same-origin here means every mail client and every other origin gets the
|
|
97
|
+
// image blocked at fetch while curl still sees 200 — the exact failure this
|
|
98
|
+
// guards. cross-origin is the only value that lets the embed through.
|
|
99
|
+
expect(res.headers.get("Cross-Origin-Resource-Policy")).toBe("cross-origin");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("keeps same-origin CORP on page responses", async () => {
|
|
103
|
+
clearRegistryHeaders();
|
|
104
|
+
const res = await appWithAssetRoute().request("/");
|
|
105
|
+
expect(res.headers.get("Cross-Origin-Resource-Policy")).toBe("same-origin");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("still serves cross-origin CORP for assets when the registry row is applied", async () => {
|
|
109
|
+
seedRegistryHeaders({ "Referrer-Policy": "strict-origin-when-cross-origin" });
|
|
110
|
+
const res = await appWithAssetRoute().request("/api/assets/org/logos/launcha.png");
|
|
111
|
+
expect(res.headers.get("Cross-Origin-Resource-Policy")).toBe("cross-origin");
|
|
112
|
+
clearRegistryHeaders();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("lets the registry row override CORP when it names the header", async () => {
|
|
116
|
+
seedRegistryHeaders({ "Cross-Origin-Resource-Policy": "same-site" });
|
|
117
|
+
const res = await appWithAssetRoute().request("/api/assets/org/logos/launcha.png");
|
|
118
|
+
expect(res.headers.get("Cross-Origin-Resource-Policy")).toBe("same-site");
|
|
119
|
+
clearRegistryHeaders();
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/security-headers.ts
CHANGED
|
@@ -35,7 +35,18 @@ const FALLBACK_CSP = [
|
|
|
35
35
|
// present some browsers enforce it over CSP. Frame control is owned solely by
|
|
36
36
|
// CSP frame-ancestors (registry security-headers config), which allows
|
|
37
37
|
// platform self-framing — e.g. the GPT panel framing a forms page.
|
|
38
|
-
const baseSecureHeaders = secureHeaders({ xFrameOptions: false });
|
|
38
|
+
const baseSecureHeaders = secureHeaders({ xFrameOptions: false, crossOriginResourcePolicy: false });
|
|
39
|
+
|
|
40
|
+
// Public brand assets are embedded BY DESIGN from other origins — most
|
|
41
|
+
// importantly report emails, which render the org logo from the serving
|
|
42
|
+
// origin. Hono's default CORP (same-origin) blocks every such embed at fetch
|
|
43
|
+
// time, so the logo silently fails to load while the URL still answers 200 to
|
|
44
|
+
// a non-browser client like curl. CORP cannot be corrected in the asset
|
|
45
|
+
// handler: Hono's `c.res` setter copies already-set middleware headers over
|
|
46
|
+
// the handler's response, so the middleware value always wins. The decision
|
|
47
|
+
// therefore lives here — cross-origin for the public asset surface,
|
|
48
|
+
// same-origin for every page and API response.
|
|
49
|
+
const CROSS_ORIGIN_EMBEDDABLE_PREFIX = "/api/assets/";
|
|
39
50
|
|
|
40
51
|
export async function injectSecurityHeaders(c: Context, next: Next) {
|
|
41
52
|
await baseSecureHeaders(c, async () => {});
|
|
@@ -55,5 +66,12 @@ export async function injectSecurityHeaders(c: Context, next: Next) {
|
|
|
55
66
|
}
|
|
56
67
|
}
|
|
57
68
|
|
|
69
|
+
if (!config?.["Cross-Origin-Resource-Policy"]) {
|
|
70
|
+
c.header(
|
|
71
|
+
"Cross-Origin-Resource-Policy",
|
|
72
|
+
new URL(c.req.url).pathname.startsWith(CROSS_ORIGIN_EMBEDDABLE_PREFIX) ? "cross-origin" : "same-origin",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
await next();
|
|
59
77
|
}
|