import { afterEach, describe, expect, it } from "vitest"; import { boundedBody, errorResponse, HttpError, readJson, requireSameOrigin, withUploadSlot } from "./http"; import { trustedClientAddress } from "./rate-limit"; const originalAuthUrl = process.env.BETTER_AUTH_URL; const originalClientIpHeader = process.env.TRUSTED_CLIENT_IP_HEADER; afterEach(() => { if (originalAuthUrl === undefined) delete process.env.BETTER_AUTH_URL; else process.env.BETTER_AUTH_URL = originalAuthUrl; if (originalClientIpHeader === undefined) delete process.env.TRUSTED_CLIENT_IP_HEADER; else process.env.TRUSTED_CLIENT_IP_HEADER = originalClientIpHeader; }); describe("request boundaries", () => { it("compares origin to configuration, not attacker-controlled Host", () => { process.env.BETTER_AUTH_URL = "https://guide.example.test"; expect(() => requireSameOrigin(new Request("https://evil.test", { headers: { origin: "https://evil.test", host: "evil.test" } }))).toThrow("cross-origin"); expect(() => requireSameOrigin(new Request("http://internal", { headers: { origin: "https://guide.example.test" } }))).not.toThrow(); expect(() => requireSameOrigin(new Request("http://internal"))).toThrow("cross-origin"); }); it("rejects actual streamed bytes even with a forged Content-Length", async () => { const body = new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(8)); controller.enqueue(new Uint8Array(8)); controller.close(); } }); const request = new Request("https://test.invalid", { method: "POST", body, headers: { "content-length": "1" }, duplex: "half", } as RequestInit & { duplex: string }); await expect(boundedBody(request, 10).arrayBuffer()).rejects.toMatchObject({ status: 413 }); }); it("rejects invalid JSON and unsupported content types", async () => { await expect(readJson(new Request("https://test.invalid", { method: "POST", body: "{}" }))).rejects.toMatchObject({ status: 415 }); await expect(readJson(new Request("https://test.invalid", { method: "POST", headers: { "content-type": "application/json" }, body: "{" }))).rejects.toMatchObject({ status: 400 }); const oversized = new Request("https://test.invalid", { method: "POST", headers: { "content-type": "application/json", "content-length": "1" }, body: JSON.stringify({ value: "a".repeat(32) }), }); await expect(readJson(oversized, 16)).rejects.toMatchObject({ status: 413 }); }); it("bounds concurrent upload work and releases slots after failure", async () => { let finish!: () => void; const blocked = new Promise((resolve) => { finish = resolve; }); const first = withUploadSlot(() => blocked); const second = withUploadSlot(() => blocked); await expect(withUploadSlot(async () => null)).rejects.toMatchObject({ status: 429 }); finish(); await Promise.all([first, second]); await expect(withUploadSlot(async () => { throw new Error("decode failed"); })).rejects.toThrow(); await expect(withUploadSlot(async () => "ok")).resolves.toBe("ok"); }); it("returns retry timing and hides internal errors", async () => { const response = errorResponse(new HttpError(429, "too-many-requests", 10)); expect(response.status).toBe(429); expect(response.headers.get("retry-after")).toBe("10"); expect(await errorResponse(new Error("secret password")).text()).not.toContain("password"); }); it("ignores spoofed forwarding headers unless explicitly configured", () => { process.env.TRUSTED_CLIENT_IP_HEADER = ""; expect(trustedClientAddress(new Headers({ "x-forwarded-for": "1.2.3.4" }))).toBe("unknown"); process.env.TRUSTED_CLIENT_IP_HEADER = "x-real-ip"; expect(trustedClientAddress(new Headers({ "x-real-ip": "1.2.3.4, 5.6.7.8" }))).toBe("unknown"); expect(trustedClientAddress(new Headers({ "x-real-ip": "::ffff:192.0.2.1" }))).toBe("192.0.2.1"); expect(trustedClientAddress(new Headers({ "x-real-ip": "2001:db8:1:2::1234" }))) .toBe(trustedClientAddress(new Headers({ "x-real-ip": "2001:0db8:0001:0002:0:0:0:1235" }))); process.env.TRUSTED_CLIENT_IP_HEADER = "x-forwarded-for"; expect(trustedClientAddress(new Headers({ "x-forwarded-for": "192.0.2.1" }))).toBe("192.0.2.1"); expect(trustedClientAddress(new Headers({ "x-forwarded-for": "192.0.2.1, 198.51.100.2" }))).toBe("unknown"); }); });