Files
scan-receipts/scripts/verify_cors.mjs
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
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>
2026-08-19 20:59:04 +02:00

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");