import { describe, test, expect, beforeEach, afterEach } from "vitest"; import { isTrustedOrigin } from "./csrf"; const originalSiteUrl = process.env.NEXT_PUBLIC_SITE_URL; beforeEach(() => { delete process.env.NEXT_PUBLIC_SITE_URL; }); afterEach(() => { if (originalSiteUrl === undefined) delete process.env.NEXT_PUBLIC_SITE_URL; else process.env.NEXT_PUBLIC_SITE_URL = originalSiteUrl; }); function makeRequest( url: string, origin?: string, extraHeaders: Record = {} ): Request { const headers = new Headers(); if (origin !== undefined) headers.set("origin", origin); for (const [key, value] of Object.entries(extraHeaders)) headers.set(key, value); return new Request(url, { method: "POST", headers }); } describe("isTrustedOrigin", () => { test("accepts a same-origin request", () => { const request = makeRequest("https://wabenhain.com/api/checkout", "https://wabenhain.com"); expect(isTrustedOrigin(request)).toBe(true); }); test("accepts a same-origin request even with an explicit default port in the URL", () => { const request = makeRequest("https://wabenhain.com:443/api/checkout", "https://wabenhain.com"); expect(isTrustedOrigin(request)).toBe(true); }); test("rejects a cross-origin request (forged CSRF attempt)", () => { const request = makeRequest("https://wabenhain.com/api/checkout", "https://attacker.example"); expect(isTrustedOrigin(request)).toBe(false); }); test("rejects a cross-origin request even from a similarly named host", () => { const request = makeRequest("https://wabenhain.com/api/checkout", "https://wabenhain.com.attacker.example"); expect(isTrustedOrigin(request)).toBe(false); }); test("rejects a malformed Origin header instead of throwing", () => { const request = makeRequest("https://wabenhain.com/api/checkout", "not-a-valid-url"); expect(isTrustedOrigin(request)).toBe(false); }); test("allows a request with no Origin header (e.g. server-to-server calls)", () => { const request = makeRequest("https://wabenhain.com/api/checkout"); expect(isTrustedOrigin(request)).toBe(true); }); test("accepts an origin matching NEXT_PUBLIC_SITE_URL even when it differs from request.url's host", () => { // Reproduziert den Live-Bug: hinter einem selbst betriebenen // Reverse-Proxy kann request.url intern einen anderen Host tragen als // den echten, oeffentlichen Host im Origin-Header — ohne diesen // Fallback wurde das faelschlich als CSRF-Versuch abgelehnt // ("Ungueltige Anfrage."), obwohl der Request vom eigenen Frontend kam. process.env.NEXT_PUBLIC_SITE_URL = "https://wabenhain.com"; const request = makeRequest("http://app:3000/api/admin/upload", "https://wabenhain.com"); expect(isTrustedOrigin(request)).toBe(true); }); test("still rejects a forged cross-origin request when NEXT_PUBLIC_SITE_URL is configured", () => { process.env.NEXT_PUBLIC_SITE_URL = "https://wabenhain.com"; const request = makeRequest("http://app:3000/api/admin/upload", "https://attacker.example"); expect(isTrustedOrigin(request)).toBe(false); }); test("accepts an origin matching the Host header even when request.url's host differs", () => { // Der eigentliche Live-Bug: Next.js rekonstruiert request.url aus der // internen Bind-Adresse des Servers, NICHT aus dem Host-Header, den der // Browser tatsaechlich adressiert hat. Ist NEXT_PUBLIC_SITE_URL nicht // (oder nicht exakt passend) gesetzt, schlug damit jeder Admin-Upload // mit "Ungueltige Anfrage." fehl, obwohl Origin und adressierter Host // identisch waren. const request = makeRequest("http://localhost:3000/api/admin/upload", "https://wabenhain.com", { host: "wabenhain.com", }); expect(isTrustedOrigin(request)).toBe(true); }); test("accepts an origin matching X-Forwarded-Host set by the reverse proxy", () => { const request = makeRequest("http://localhost:3000/api/admin/upload", "https://wabenhain.com", { host: "app:3000", "x-forwarded-host": "wabenhain.com", }); expect(isTrustedOrigin(request)).toBe(true); }); test("uses only the first entry of a comma-separated X-Forwarded-Host chain", () => { const request = makeRequest("http://localhost:3000/api/admin/upload", "https://wabenhain.com", { "x-forwarded-host": "wabenhain.com, internal.example", }); expect(isTrustedOrigin(request)).toBe(true); }); test("rejects a forged cross-origin request even though the Host header is trusted", () => { // Gegenprobe zur Host-Header-Pruefung: ein Cross-Site-POST traegt zwar // den echten Ziel-Host (der Browser setzt ihn, nicht die Angreiferseite), // aber den fremden Origin — muss weiterhin abgelehnt werden. const request = makeRequest("http://localhost:3000/api/admin/upload", "https://attacker.example", { host: "wabenhain.com", }); expect(isTrustedOrigin(request)).toBe(false); }); });