83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
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([]);
|
|
});
|
|
});
|