chore: ran security check for OWASP top 10
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2026-03-01 20:44:55 -06:00
parent 023587c48c
commit 1645896e54
20 changed files with 1916 additions and 168 deletions

View 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([]);
});
});