import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { readFileSync, readdirSync, statSync } from "node:fs"; import { join, resolve } from "node:path"; const ORIGINAL_ENV = { ...process.env }; beforeEach(() => { vi.resetModules(); vi.resetAllMocks(); process.env = { ...ORIGINAL_ENV }; }); afterAll(() => { process.env = ORIGINAL_ENV; }); function setRequiredBaseEnv() { process.env.DATABASE_URL = "postgres://app:app@127.0.0.1:5432/skymoney"; process.env.JWT_SECRET = "test-jwt-secret-32-chars-min-abcdef"; process.env.COOKIE_SECRET = "test-cookie-secret-32-chars-abcdef"; process.env.CORS_ORIGINS = "https://allowed.example.com"; process.env.AUTH_DISABLED = "0"; process.env.SEED_DEFAULT_BUDGET = "0"; } describe("A10 Server-Side Request Forgery", () => { it("rejects localhost/private APP_ORIGIN values in production", async () => { const blockedOrigins = [ "https://127.0.0.1:5173", "https://10.0.0.5:5173", "https://192.168.1.10:5173", "https://172.16.5.20:5173", "https://localhost:5173", "https://app.local:5173", "https://[::1]:5173", ]; for (const origin of blockedOrigins) { setRequiredBaseEnv(); process.env.NODE_ENV = "production"; process.env.APP_ORIGIN = origin; await expect(import("../src/env")).rejects.toThrow( "APP_ORIGIN must not point to localhost or private network hosts in production." ); vi.resetModules(); } }); it("accepts public APP_ORIGIN in production", async () => { setRequiredBaseEnv(); process.env.NODE_ENV = "production"; process.env.APP_ORIGIN = "https://app.example.com"; const envModule = await import("../src/env"); expect(envModule.env.APP_ORIGIN).toBe("https://app.example.com"); }); it("keeps API source free of generic outbound HTTP request clients", () => { const apiRoot = resolve(__dirname, ".."); const srcRoot = resolve(apiRoot, "src"); const queue = [srcRoot]; const offenders: string[] = []; const patterns = ["fetch(", "axios", "http.request(", "https.request("]; while (queue.length > 0) { const current = queue.pop() as string; for (const name of readdirSync(current)) { const full = join(current, name); if (statSync(full).isDirectory()) { if (full.includes(`${join("src", "scripts")}`)) continue; queue.push(full); continue; } if (!full.endsWith(".ts")) continue; const content = readFileSync(full, "utf8"); if (patterns.some((p) => content.includes(p))) offenders.push(full); } } expect(offenders).toEqual([]); }); });