chore: ran security check for OWASP top 10
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
This commit is contained in:
82
api/tests/server-side-request-forgery.test.ts
Normal file
82
api/tests/server-side-request-forgery.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user