first commit
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { withSecurityHeaders, isWebSocketOriginAllowed } from "../src/index.ts";
|
||||
|
||||
const req = (headers: Record<string, string> = {}) => new Request("https://x/", { headers });
|
||||
|
||||
function scriptSrc(csp: string): string {
|
||||
return csp
|
||||
.split(";")
|
||||
.map((s) => s.trim())
|
||||
.find((s) => s.startsWith("script-src"))!;
|
||||
}
|
||||
|
||||
test("CSP nonce is added to script-src and drops unsafe-inline", () => {
|
||||
const res = withSecurityHeaders(req(), new Response("x"), "development", undefined, "ABC123");
|
||||
const directive = scriptSrc(res.headers.get("content-security-policy")!);
|
||||
expect(directive).toContain("'nonce-ABC123'");
|
||||
expect(directive).not.toContain("'unsafe-inline'");
|
||||
});
|
||||
|
||||
test("without a nonce, dev script-src keeps unsafe-inline (for HMR)", () => {
|
||||
const res = withSecurityHeaders(req(), new Response("x"), "development");
|
||||
expect(scriptSrc(res.headers.get("content-security-policy")!)).toContain("'unsafe-inline'");
|
||||
});
|
||||
|
||||
test("CORS credentials + origin:* is refused (credentials dropped)", () => {
|
||||
const res = withSecurityHeaders(
|
||||
req({ origin: "https://evil.test" }),
|
||||
new Response("x"),
|
||||
"production",
|
||||
{
|
||||
cors: { enabled: true, origin: "*", credentials: true },
|
||||
},
|
||||
);
|
||||
expect(res.headers.get("access-control-allow-credentials")).toBeNull();
|
||||
});
|
||||
|
||||
test("production sets HSTS + strict CSP", () => {
|
||||
const res = withSecurityHeaders(req(), new Response("x"), "production");
|
||||
expect(res.headers.get("strict-transport-security")).toContain("max-age=");
|
||||
expect(res.headers.get("content-security-policy")).toContain("default-src 'self'");
|
||||
});
|
||||
|
||||
test("permissions policy overrides merge with restrictive defaults", () => {
|
||||
const res = withSecurityHeaders(req(), new Response("x"), "development", {
|
||||
permissionsPolicy: { camera: ["self"] },
|
||||
});
|
||||
const policy = res.headers.get("permissions-policy")!;
|
||||
expect(policy).toContain("camera=(self)");
|
||||
expect(policy).toContain("microphone=()");
|
||||
});
|
||||
|
||||
test("isWebSocketOriginAllowed blocks cross-site WS (CSWSH), allows same-origin", () => {
|
||||
const wsReq = (origin: string | null, host: string) =>
|
||||
new Request("http://x/realtime/c", {
|
||||
headers: origin ? { origin, host } : { host },
|
||||
});
|
||||
expect(isWebSocketOriginAllowed(wsReq("http://app.test", "app.test"))).toBe(true); // same-origin
|
||||
expect(isWebSocketOriginAllowed(wsReq("http://evil.test", "app.test"))).toBe(false); // cross-site
|
||||
expect(isWebSocketOriginAllowed(wsReq(null, "app.test"))).toBe(true); // native client, no cookies
|
||||
// Explicit CORS allowlist opens a cross-origin WS.
|
||||
expect(
|
||||
isWebSocketOriginAllowed(wsReq("http://other.test", "app.test"), {
|
||||
cors: { enabled: true, origin: "http://other.test" },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
Reference in New Issue
Block a user