Files
SkyMoney/api/tests/security-misconfiguration.test.ts
Ricearoni1245 1645896e54
Some checks failed
Deploy / deploy (push) Has been cancelled
chore: ran security check for OWASP top 10
2026-03-01 20:44:55 -06:00

151 lines
5.0 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
afterEach(async () => {
vi.resetAllMocks();
vi.resetModules();
vi.doUnmock("nodemailer");
});
describe("A02 Security Misconfiguration", () => {
it("enforces CORS allowlist in production", async () => {
const { buildApp } = await import("../src/server");
const app = await buildApp({
NODE_ENV: "production",
CORS_ORIGINS: "https://allowed.example.com",
AUTH_DISABLED: false,
SEED_DEFAULT_BUDGET: false,
APP_ORIGIN: "https://allowed.example.com",
});
await app.ready();
try {
const allowedOrigin = "https://allowed.example.com";
const deniedOrigin = "https://denied.example.com";
const allowed = await app.inject({
method: "OPTIONS",
url: "/health",
headers: {
origin: allowedOrigin,
"access-control-request-method": "GET",
},
});
expect(allowed.statusCode).toBe(204);
expect(allowed.headers["access-control-allow-origin"]).toBe(allowedOrigin);
expect(allowed.headers["access-control-allow-credentials"]).toBe("true");
const denied = await app.inject({
method: "OPTIONS",
url: "/health",
headers: {
origin: deniedOrigin,
"access-control-request-method": "GET",
},
});
expect([204, 404]).toContain(denied.statusCode);
expect(denied.headers["access-control-allow-origin"]).toBeUndefined();
expect(denied.headers["access-control-allow-credentials"]).toBeUndefined();
} finally {
await app.close();
}
});
it("applies expected CORS response headers at runtime for allowed origins", async () => {
const { buildApp } = await import("../src/server");
const app = await buildApp({
NODE_ENV: "production",
CORS_ORIGINS: "https://allowed.example.com",
AUTH_DISABLED: false,
SEED_DEFAULT_BUDGET: false,
APP_ORIGIN: "https://allowed.example.com",
});
await app.ready();
try {
const response = await app.inject({
method: "OPTIONS",
url: "/health",
headers: {
origin: "https://allowed.example.com",
"access-control-request-method": "GET",
},
});
expect(response.statusCode).toBe(204);
expect(response.headers["access-control-allow-origin"]).toBe(
"https://allowed.example.com"
);
expect(response.headers["access-control-allow-credentials"]).toBe("true");
expect(response.headers.vary).toContain("Origin");
} finally {
await app.close();
}
});
it("disables SMTP debug logger in production mailer config", async () => {
const verify = vi.fn(async () => undefined);
const sendMail = vi.fn(async () => ({ messageId: "mock-message-id" }));
const createTransport = vi.fn(() => ({ verify, sendMail }));
vi.doMock("nodemailer", () => ({
default: { createTransport },
}));
const { buildApp } = await import("../src/server");
const app = await buildApp({
NODE_ENV: "production",
SMTP_HOST: "smtp.example.com",
SMTP_PORT: 587,
SMTP_REQUIRE_TLS: true,
SMTP_TLS_REJECT_UNAUTHORIZED: true,
SMTP_USER: "smtp-user",
SMTP_PASS: "smtp-pass",
CORS_ORIGINS: "https://allowed.example.com",
APP_ORIGIN: "https://allowed.example.com",
AUTH_DISABLED: false,
SEED_DEFAULT_BUDGET: false,
});
try {
await app.ready();
expect(createTransport).toHaveBeenCalledTimes(1);
const transportOptions = createTransport.mock.calls[0]?.[0] as Record<string, unknown>;
expect(transportOptions.requireTLS).toBe(true);
expect(transportOptions.logger).toBe(false);
expect(transportOptions.debug).toBe(false);
} finally {
await app.close();
}
});
it("defines anti-framing headers in edge configs", () => {
const root = resolve(__dirname, "..", "..");
const caddyProd = readFileSync(resolve(root, "Caddyfile.prod"), "utf8");
const caddyDev = readFileSync(resolve(root, "Caddyfile.dev"), "utf8");
const nginxProd = readFileSync(
resolve(root, "deploy/nginx/skymoneybudget.com.conf"),
"utf8"
);
expect(caddyProd).toContain('X-Frame-Options "DENY"');
expect(caddyProd).toContain(`Content-Security-Policy "frame-ancestors 'none'"`);
expect(caddyDev).toContain('X-Frame-Options "DENY"');
expect(caddyDev).toContain(`Content-Security-Policy "frame-ancestors 'none'"`);
expect(nginxProd).toContain('add_header X-Frame-Options "DENY" always;');
expect(nginxProd).toContain(
`add_header Content-Security-Policy "frame-ancestors 'none'" always;`
);
});
it("keeps docker-compose service ports bound to localhost only", () => {
const root = resolve(__dirname, "..", "..");
const compose = readFileSync(resolve(root, "docker-compose.yml"), "utf8");
expect(compose).toContain('- "127.0.0.1:5432:5432"');
expect(compose).toContain('- "127.0.0.1:8081:8080"');
});
});