Files
buzz-sheet/lib/security/http.test.ts
T
gunshiz 00bbc4e4ca
CI / Verify (push) Failing after 1m16s
CI / Build immutable images and deploy (push) Skipped
feat : it work on my machice
2026-10-10 03:24:34 +07:00

92 lines
5.2 KiB
TypeScript

import { afterEach, describe, expect, it, vi } 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(() => {
vi.restoreAllMocks();
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<Uint8Array>({ 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<void>((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("preserves explicit HTTP errors and retry timing", 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 response.json()).toEqual({ error: "too-many-requests" });
const unavailable = errorResponse(new HttpError(503, "dependency-unavailable", 5));
expect(unavailable.status).toBe(503);
expect(unavailable.headers.get("retry-after")).toBe("5");
expect(await unavailable.json()).toEqual({ error: "dependency-unavailable" });
});
it.each([new Error("secret password"), "unexpected thrown value", null])("returns 500 for unexpected errors and logs the original cause: %s", async (cause) => {
const log = vi.spyOn(console, "error").mockImplementation(() => {});
const response = errorResponse(cause);
expect(response.status).toBe(500);
expect(await response.json()).toEqual({ error: "internal-server-error" });
expect(response.headers.get("cache-control")).toBe("no-store");
expect(response.headers.get("retry-after")).toBeNull();
expect(log).toHaveBeenCalledWith("Unhandled request error", cause);
});
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");
});
});