Brings the working codebase (Next.js app, auth system, Stripe billing, Docker/deploy config, tests, docs) into version control on top of the placeholder initial commit, and adds account self-deletion (Danger Zone in Settings, password + typed-email confirmation, cascading DB cleanup, Stripe cancellation) per GDPR right-to-erasure. Excludes local build caches, node_modules, and internal agent scratch files; .gitignore hardened to keep those out going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
346 lines
13 KiB
JavaScript
346 lines
13 KiB
JavaScript
/**
|
|
* CORS policy verification harness.
|
|
*
|
|
* Exercises the pure helpers in src/lib/http/cors.ts with simulated requests
|
|
* (real NextRequest/NextResponse instances) and asserts the policy:
|
|
* - preflight from an allowed origin -> 204 + correct headers
|
|
* - preflight from a disallowed origin -> 403
|
|
* - normal request, disallowed origin -> 403
|
|
* - no Origin header / allowed origin -> undefined (continue)
|
|
* - allowlist normalization (trailing slash, case, default port)
|
|
* - never emits `Access-Control-Allow-Origin: *`
|
|
*
|
|
* Run: node scripts/verify_cors.mjs
|
|
* (Node >= 23.6 / 24 runs the imported .ts directly via built-in type stripping;
|
|
* no tsx required.)
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
import { NextRequest } from "next/server";
|
|
import {
|
|
getAllowedOrigins,
|
|
isOriginAllowed,
|
|
corsHeadersFor,
|
|
handleCors,
|
|
originComparisonKey,
|
|
CORS_ALLOWED_METHODS,
|
|
CORS_ERROR_CODE,
|
|
} from "../src/lib/http/cors.ts";
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function check(name, fn) {
|
|
try {
|
|
fn();
|
|
passed += 1;
|
|
console.log(` ok ${name}`);
|
|
} catch (err) {
|
|
failed += 1;
|
|
console.error(`FAIL ${name}`);
|
|
console.error(String(err instanceof Error ? err.stack : err).replace(/^/gm, " "));
|
|
}
|
|
}
|
|
|
|
/** Env that looks like production: app origin + a few extra allowlisted origins. */
|
|
const PROD_ENV = {
|
|
CORS_ORIGINS: "https://admin.example.com, http://localhost:8080/",
|
|
NEXT_PUBLIC_APP_URL: "https://app.example.com",
|
|
};
|
|
|
|
const APP_ORIGIN = "https://app.example.com";
|
|
|
|
function req(method, headers, url = "https://app.example.com/api/receipts") {
|
|
return new NextRequest(url, { method, headers });
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
console.log("CORS policy verification\n");
|
|
console.log("1) Allowlist construction & normalization");
|
|
|
|
check("getAllowedOrigins returns full origins (echo form), scheme & case preserved", () => {
|
|
const list = getAllowedOrigins(PROD_ENV);
|
|
assert.ok(list.includes("https://app.example.com"), `NEXT_PUBLIC_APP_URL missing: ${list}`);
|
|
assert.ok(list.includes("https://admin.example.com"), `missing admin origin: ${list}`);
|
|
assert.ok(list.includes("http://localhost:8080"), `missing localhost origin: ${list}`);
|
|
// Trailing slashes are stripped during normalization (spec).
|
|
assert.ok(!list.some((o) => o.endsWith("/")), `trailing slash survived: ${list}`);
|
|
});
|
|
|
|
check("allowlist is de-duplicated by comparison key; app origin wins over duplicates", () => {
|
|
const dupEnv = {
|
|
CORS_ORIGINS: "https://app.example.com, https://APP.EXAMPLE.COM:443",
|
|
NEXT_PUBLIC_APP_URL: "https://app.example.com",
|
|
};
|
|
// All three spellings share the key "app.example.com" -> exactly one entry,
|
|
// and it is the canonical NEXT_PUBLIC_APP_URL spelling (added first).
|
|
const list = getAllowedOrigins(dupEnv);
|
|
assert.deepEqual(list, ["https://app.example.com"], `expected single canonical entry: ${list}`);
|
|
const keys = getAllowedOrigins(PROD_ENV).map(originComparisonKey);
|
|
assert.equal(new Set(keys).size, keys.length, `duplicate keys: ${keys}`);
|
|
});
|
|
|
|
check("comparison key strips scheme, trailing slash, lowercases host, collapses default port", () => {
|
|
assert.equal(originComparisonKey("https://App.Example.com/"), "app.example.com");
|
|
assert.equal(originComparisonKey("https://app.example.com:443"), "app.example.com");
|
|
assert.equal(originComparisonKey("http://app.example.com:80"), "app.example.com");
|
|
assert.equal(originComparisonKey("HTTPS://app.example.com:3000/"), "app.example.com:3000");
|
|
assert.equal(originComparisonKey("http://localhost:8080"), "localhost:8080");
|
|
});
|
|
|
|
check("comparison key drops path/query/fragment defensively", () => {
|
|
assert.equal(
|
|
originComparisonKey("https://app.example.com/some/path?x=1#frag"),
|
|
"app.example.com"
|
|
);
|
|
});
|
|
|
|
check("comparison key handles IPv6 literals", () => {
|
|
assert.equal(originComparisonKey("http://[::1]:3000"), "[::1]:3000");
|
|
assert.equal(originComparisonKey("http://[::1]"), "[::1]");
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
console.log("\n2) isOriginAllowed");
|
|
|
|
check("no Origin header (null/empty) is allowed (curl, same-origin, server-to-server)", () => {
|
|
assert.equal(isOriginAllowed(null, PROD_ENV), true);
|
|
assert.equal(isOriginAllowed("", PROD_ENV), true);
|
|
});
|
|
|
|
check("exact allowed origin matches", () => {
|
|
assert.equal(isOriginAllowed(APP_ORIGIN, PROD_ENV), true);
|
|
assert.equal(isOriginAllowed("https://admin.example.com", PROD_ENV), true);
|
|
assert.equal(isOriginAllowed("http://localhost:8080", PROD_ENV), true);
|
|
});
|
|
|
|
check("normalization: trailing slash, mixed case, explicit default port all match", () => {
|
|
assert.equal(isOriginAllowed("https://app.example.com/", PROD_ENV), true);
|
|
assert.equal(isOriginAllowed("HTTPS://APP.EXAMPLE.COM", PROD_ENV), true);
|
|
assert.equal(isOriginAllowed("https://app.example.com:443", PROD_ENV), true);
|
|
});
|
|
|
|
check("disallowed origins are rejected (never *, exact match only)", () => {
|
|
assert.equal(isOriginAllowed("https://evil.example.com", PROD_ENV), false);
|
|
assert.equal(isOriginAllowed("https://example.com", PROD_ENV), false);
|
|
assert.equal(isOriginAllowed("http://localhost:9999", PROD_ENV), false);
|
|
assert.equal(isOriginAllowed("null", PROD_ENV), false);
|
|
});
|
|
|
|
check("substring / suffix attacks do not match", () => {
|
|
assert.equal(isOriginAllowed("https://app.example.com.evil.com", PROD_ENV), false);
|
|
assert.equal(isOriginAllowed("https://app.example.com.evil.com:443", PROD_ENV), false);
|
|
assert.equal(isOriginAllowed("https://evilexample.com", PROD_ENV), false);
|
|
assert.equal(isOriginAllowed("https://admin.example.com.evil.com", PROD_ENV), false);
|
|
});
|
|
|
|
check("host + port are exact; scheme is ignored for comparison (documented policy)", () => {
|
|
// Port is part of the exact match.
|
|
assert.equal(isOriginAllowed("https://app.example.com:444", PROD_ENV), false);
|
|
assert.equal(isOriginAllowed("http://localhost:8081", PROD_ENV), false);
|
|
// The http/https prefix is stripped for comparison (policy), so the same
|
|
// host:port under the other scheme is treated as the same origin.
|
|
assert.equal(isOriginAllowed("http://app.example.com", PROD_ENV), true);
|
|
});
|
|
|
|
check("empty allowlist => same-origin only (everything with an Origin rejected)", () => {
|
|
const empty = { CORS_ORIGINS: "", NEXT_PUBLIC_APP_URL: "" };
|
|
assert.equal(isOriginAllowed(null, empty), true);
|
|
assert.equal(isOriginAllowed("https://app.example.com", empty), false);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
console.log("\n3) corsHeadersFor");
|
|
|
|
check("allowed origin -> exact echo + Vary: Origin + credentials; never '*'", () => {
|
|
const h = corsHeadersFor(APP_ORIGIN, PROD_ENV);
|
|
assert.equal(h["Access-Control-Allow-Origin"], APP_ORIGIN);
|
|
assert.equal(h["Vary"], "Origin");
|
|
assert.equal(h["Access-Control-Allow-Credentials"], "true");
|
|
assert.ok(!Object.values(h).includes("*"), "wildcard leaked into headers");
|
|
});
|
|
|
|
check("disallowed / missing origin -> no CORS headers", () => {
|
|
assert.deepEqual(corsHeadersFor("https://evil.example.com", PROD_ENV), {});
|
|
assert.deepEqual(corsHeadersFor(null, PROD_ENV), {});
|
|
assert.deepEqual(corsHeadersFor("", PROD_ENV), {});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
console.log("\n4) handleCors — preflight");
|
|
|
|
check("preflight from allowed origin -> 204 with full header set", () => {
|
|
const res = handleCors(
|
|
req("OPTIONS", {
|
|
origin: APP_ORIGIN,
|
|
"access-control-request-method": "POST",
|
|
"access-control-request-headers": "content-type, authorization",
|
|
}),
|
|
PROD_ENV
|
|
);
|
|
assert.ok(res, "expected a response");
|
|
assert.equal(res.status, 204);
|
|
assert.equal(res.headers.get("access-control-allow-origin"), APP_ORIGIN);
|
|
assert.equal(res.headers.get("vary"), "Origin");
|
|
assert.equal(res.headers.get("access-control-allow-credentials"), "true");
|
|
assert.equal(res.headers.get("access-control-allow-methods"), CORS_ALLOWED_METHODS);
|
|
assert.equal(res.headers.get("access-control-max-age"), "600");
|
|
assert.equal(res.headers.get("access-control-allow-headers"), "content-type, authorization");
|
|
});
|
|
|
|
check("preflight echo uses the exact request origin, never a wildcard", () => {
|
|
const res = handleCors(
|
|
req("OPTIONS", {
|
|
origin: "https://admin.example.com",
|
|
"access-control-request-method": "GET",
|
|
}),
|
|
PROD_ENV
|
|
);
|
|
assert.ok(res);
|
|
assert.equal(res.status, 204);
|
|
const acao = res.headers.get("access-control-allow-origin");
|
|
assert.equal(acao, "https://admin.example.com");
|
|
assert.ok(acao && !acao.includes("*"), "Access-Control-Allow-Origin contains '*'");
|
|
});
|
|
|
|
check("preflight from disallowed origin -> 403, no ACAO header", () => {
|
|
const res = handleCors(
|
|
req("OPTIONS", {
|
|
origin: "https://evil.example.com",
|
|
"access-control-request-method": "POST",
|
|
}),
|
|
PROD_ENV
|
|
);
|
|
assert.ok(res);
|
|
assert.equal(res.status, 403);
|
|
assert.equal(res.headers.get("access-control-allow-origin"), null, "no ACAO on refusal");
|
|
});
|
|
|
|
check("preflight 403 carries the machine-readable error code", async () => {
|
|
const res = handleCors(
|
|
req("OPTIONS", {
|
|
origin: "https://evil.example.com",
|
|
"access-control-request-method": "POST",
|
|
}),
|
|
PROD_ENV
|
|
);
|
|
assert.ok(res);
|
|
const body = await res.json();
|
|
assert.deepEqual(body, { error: CORS_ERROR_CODE });
|
|
});
|
|
|
|
check("Access-Control-Allow-Headers echo is validated (invalid tokens dropped)", () => {
|
|
const res = handleCors(
|
|
req("OPTIONS", {
|
|
origin: APP_ORIGIN,
|
|
"access-control-request-method": "PUT",
|
|
"access-control-request-headers": "content-type, bad header!, x-foo",
|
|
}),
|
|
PROD_ENV
|
|
);
|
|
assert.ok(res);
|
|
assert.equal(res.headers.get("access-control-allow-headers"), "content-type, x-foo");
|
|
});
|
|
|
|
check("Access-Control-Allow-Headers echo is bounded (oversized input omitted)", () => {
|
|
const res = handleCors(
|
|
req("OPTIONS", {
|
|
origin: APP_ORIGIN,
|
|
"access-control-request-method": "PUT",
|
|
"access-control-request-headers": "x-big-" + "a".repeat(5000),
|
|
}),
|
|
PROD_ENV
|
|
);
|
|
assert.ok(res);
|
|
assert.equal(res.headers.get("access-control-allow-headers"), null);
|
|
});
|
|
|
|
check("Access-Control-Allow-Headers echo is de-duplicated", () => {
|
|
const res = handleCors(
|
|
req("OPTIONS", {
|
|
origin: APP_ORIGIN,
|
|
"access-control-request-method": "POST",
|
|
"access-control-request-headers": "content-type, content-type, authorization",
|
|
}),
|
|
PROD_ENV
|
|
);
|
|
assert.ok(res);
|
|
assert.equal(res.headers.get("access-control-allow-headers"), "content-type, authorization");
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
console.log("\n5) handleCors — non-preflight");
|
|
|
|
check("normal request from allowed origin -> undefined (continue)", () => {
|
|
const res = handleCors(req("GET", { origin: APP_ORIGIN }), PROD_ENV);
|
|
assert.equal(res, undefined);
|
|
});
|
|
|
|
check("normal request with no Origin -> undefined (continue)", () => {
|
|
const res = handleCors(req("GET", {}), PROD_ENV);
|
|
assert.equal(res, undefined);
|
|
});
|
|
|
|
check("normal request from disallowed origin -> 403", () => {
|
|
const res = handleCors(req("GET", { origin: "https://evil.example.com" }), PROD_ENV);
|
|
assert.ok(res);
|
|
assert.equal(res.status, 403);
|
|
});
|
|
|
|
check("bare OPTIONS without Access-Control-Request-Method is not a preflight", () => {
|
|
// Disallowed origin on a bare OPTIONS is still refused (cross-origin).
|
|
const disallowed = handleCors(req("OPTIONS", { origin: "https://evil.example.com" }), PROD_ENV);
|
|
assert.ok(disallowed);
|
|
assert.equal(disallowed.status, 403);
|
|
// Allowed origin on a bare OPTIONS passes through.
|
|
const allowed = handleCors(req("OPTIONS", { origin: APP_ORIGIN }), PROD_ENV);
|
|
assert.equal(allowed, undefined);
|
|
});
|
|
|
|
check("POST (credentials path) from allowed origin -> undefined", () => {
|
|
const res = handleCors(req("POST", { origin: APP_ORIGIN }), PROD_ENV);
|
|
assert.equal(res, undefined);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
console.log("\n6) global: no '*' is ever emitted as a header value");
|
|
|
|
function collectHeaderValues() {
|
|
const values = [];
|
|
const origins = ["https://app.example.com", "https://admin.example.com", "http://localhost:8080"];
|
|
for (const origin of origins) {
|
|
const preflight = handleCors(
|
|
req("OPTIONS", {
|
|
origin,
|
|
"access-control-request-method": "GET",
|
|
"access-control-request-headers": "content-type, authorization",
|
|
}),
|
|
PROD_ENV
|
|
);
|
|
if (preflight) preflight.headers.forEach((v) => values.push(v));
|
|
values.push(...Object.values(corsHeadersFor(origin, PROD_ENV)));
|
|
}
|
|
return values;
|
|
}
|
|
|
|
check("no emitted header value equals or contains '*'", () => {
|
|
const values = collectHeaderValues();
|
|
assert.ok(values.length > 0, "expected to collect some header values");
|
|
for (const v of values) {
|
|
assert.ok(!v.includes("*"), `wildcard value emitted: ${JSON.stringify(v)}`);
|
|
}
|
|
});
|
|
|
|
check("allowlist itself contains no wildcard entries", () => {
|
|
for (const origin of getAllowedOrigins(PROD_ENV)) {
|
|
assert.ok(!origin.includes("*"), `wildcard in allowlist: ${origin}`);
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
if (failed > 0) {
|
|
console.error("CORS verification FAILED");
|
|
process.exit(1);
|
|
}
|
|
console.log("CORS verification PASSED");
|