83 lines
3.7 KiB
JavaScript
83 lines
3.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import test from "node:test";
|
|
import {
|
|
CONTENT_SECURITY_POLICY,
|
|
PERMISSIONS_POLICY,
|
|
withSecurityHeaders,
|
|
} from "../worker/security-headers.ts";
|
|
|
|
test("applies the browser security baseline without changing the response", async () => {
|
|
const request = new Request("https://cloud.example.test/dashboard");
|
|
const response = withSecurityHeaders(
|
|
request,
|
|
new Response("ok", {
|
|
status: 201,
|
|
headers: {
|
|
"Cache-Control": "private, no-store",
|
|
"Content-Type": "text/plain; charset=utf-8",
|
|
},
|
|
}),
|
|
);
|
|
|
|
assert.equal(response.status, 201);
|
|
assert.equal(await response.text(), "ok");
|
|
assert.equal(response.headers.get("Cache-Control"), "private, no-store");
|
|
assert.equal(response.headers.get("Content-Type"), "text/plain; charset=utf-8");
|
|
assert.equal(response.headers.get("Content-Security-Policy"), CONTENT_SECURITY_POLICY);
|
|
assert.equal(response.headers.get("Permissions-Policy"), PERMISSIONS_POLICY);
|
|
assert.equal(response.headers.get("Referrer-Policy"), "no-referrer");
|
|
assert.equal(response.headers.get("X-Content-Type-Options"), "nosniff");
|
|
assert.equal(response.headers.get("X-DNS-Prefetch-Control"), "off");
|
|
assert.equal(response.headers.get("X-Frame-Options"), "DENY");
|
|
assert.equal(response.headers.get("X-Permitted-Cross-Domain-Policies"), "none");
|
|
assert.equal(response.headers.get("Strict-Transport-Security"), "max-age=31536000");
|
|
});
|
|
|
|
test("keeps the baseline CSP restrictive while documenting its inline compatibility exception", () => {
|
|
assert.match(CONTENT_SECURITY_POLICY, /default-src 'self'/);
|
|
assert.match(CONTENT_SECURITY_POLICY, /object-src 'none'/);
|
|
assert.match(CONTENT_SECURITY_POLICY, /frame-ancestors 'none'/);
|
|
assert.match(CONTENT_SECURITY_POLICY, /form-action 'self'/);
|
|
assert.match(CONTENT_SECURITY_POLICY, /script-src 'self' 'unsafe-inline'/);
|
|
assert.doesNotMatch(CONTENT_SECURITY_POLICY, /unsafe-eval/);
|
|
assert.doesNotMatch(CONTENT_SECURITY_POLICY, /script-src[^;]*(?:\*|https:)/);
|
|
assert.match(PERMISSIONS_POLICY, /camera=\(\)/);
|
|
assert.match(PERMISSIONS_POLICY, /microphone=\(\)/);
|
|
assert.match(PERMISSIONS_POLICY, /payment=\(\)/);
|
|
});
|
|
|
|
test("never advertises HSTS on a plaintext development origin", () => {
|
|
const response = withSecurityHeaders(
|
|
new Request("http://127.0.0.1:3000/"),
|
|
new Response(null, {
|
|
headers: { "Strict-Transport-Security": "max-age=999999" },
|
|
}),
|
|
);
|
|
|
|
assert.equal(response.headers.get("Strict-Transport-Security"), null);
|
|
assert.equal(response.headers.get("X-Frame-Options"), "DENY");
|
|
});
|
|
|
|
test("prevents browsers and intermediaries from caching account and API data", () => {
|
|
for (const path of ["/api/account/export", "/dashboard", "/dashboard/hosts", "/admin"]) {
|
|
const response = withSecurityHeaders(
|
|
new Request(`https://cloud.example.test${path}`),
|
|
new Response("private", { headers: { "Cache-Control": "public, max-age=3600" } }),
|
|
);
|
|
assert.equal(response.headers.get("Cache-Control"), "private, no-store");
|
|
}
|
|
|
|
const publicResponse = withSecurityHeaders(
|
|
new Request("https://cloud.example.test/trust"),
|
|
new Response("public", { headers: { "Cache-Control": "public, max-age=300" } }),
|
|
);
|
|
assert.equal(publicResponse.headers.get("Cache-Control"), "public, max-age=300");
|
|
});
|
|
|
|
test("wraps both application and image responses at the Worker exit", async () => {
|
|
const source = await readFile(new URL("../worker/index.ts", import.meta.url), "utf8");
|
|
assert.match(source, /return withSecurityHeaders\(request, response\);[\s\S]*handler\.fetch/);
|
|
assert.match(source, /handler\.fetch[\s\S]*return withSecurityHeaders\(request, response\);/);
|
|
});
|