Files
nekonest-cloud/tests/security-headers.test.mjs

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\);/);
});