Files
SkyMoney/api/tests/cryptographic-failures.runtime.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

146 lines
4.2 KiB
TypeScript

import { createHmac } from "node:crypto";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import request from "supertest";
import type { FastifyInstance } from "fastify";
import { buildApp } from "../src/server";
const JWT_SECRET = "test-jwt-secret-32-chars-min-abcdef";
const COOKIE_SECRET = "test-cookie-secret-32-chars-abcdef";
let app: FastifyInstance;
function base64UrlEncode(value: string): string {
return Buffer.from(value)
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
function signHs256Token(payload: Record<string, unknown>, secret: string): string {
const header = { alg: "HS256", typ: "JWT" };
const encodedHeader = base64UrlEncode(JSON.stringify(header));
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
const signingInput = `${encodedHeader}.${encodedPayload}`;
const signature = createHmac("sha256", secret)
.update(signingInput)
.digest("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
return `${signingInput}.${signature}`;
}
beforeAll(async () => {
app = await buildApp({
NODE_ENV: "test",
AUTH_DISABLED: false,
SEED_DEFAULT_BUDGET: false,
DATABASE_URL: "postgres://app:app@127.0.0.1:5432/skymoney",
JWT_SECRET,
COOKIE_SECRET,
JWT_ISSUER: "skymoney-api",
JWT_AUDIENCE: "skymoney-web",
APP_ORIGIN: "http://localhost:5173",
});
await app.ready();
// Keep JWT verification behavior under test without requiring DB connectivity.
(app as any).ensureUser = async () => undefined;
});
afterAll(async () => {
if (app) await app.close();
});
describe("A04 Cryptographic Failures (runtime adversarial checks)", () => {
const csrf = "runtime-test-csrf-token";
it("rejects token with wrong issuer", async () => {
const nowSeconds = Math.floor(Date.now() / 1000);
const token = signHs256Token(
{
sub: `wrong-iss-${Date.now()}`,
iss: "attacker-issuer",
aud: "skymoney-web",
iat: nowSeconds,
exp: nowSeconds + 600,
},
JWT_SECRET
);
const res = await request(app.server)
.post("/auth/refresh")
.set("x-csrf-token", csrf)
.set("Cookie", [`csrf=${csrf}`, `session=${token}`]);
expect(res.status).toBe(401);
expect(res.body.code).toBe("UNAUTHENTICATED");
});
it("rejects token with wrong audience", async () => {
const nowSeconds = Math.floor(Date.now() / 1000);
const token = signHs256Token(
{
sub: `wrong-aud-${Date.now()}`,
iss: "skymoney-api",
aud: "attacker-app",
iat: nowSeconds,
exp: nowSeconds + 600,
},
JWT_SECRET
);
const res = await request(app.server)
.post("/auth/refresh")
.set("x-csrf-token", csrf)
.set("Cookie", [`csrf=${csrf}`, `session=${token}`]);
expect(res.status).toBe(401);
expect(res.body.code).toBe("UNAUTHENTICATED");
});
it("rejects token with alg=none (unsigned token)", async () => {
const nowSeconds = Math.floor(Date.now() / 1000);
const encodedHeader = base64UrlEncode(JSON.stringify({ alg: "none", typ: "JWT" }));
const encodedPayload = base64UrlEncode(
JSON.stringify({
sub: `none-alg-${Date.now()}`,
iss: "skymoney-api",
aud: "skymoney-web",
iat: nowSeconds,
exp: nowSeconds + 600,
})
);
const token = `${encodedHeader}.${encodedPayload}.`;
const res = await request(app.server)
.post("/auth/refresh")
.set("x-csrf-token", csrf)
.set("Cookie", [`csrf=${csrf}`, `session=${token}`]);
expect(res.status).toBe(401);
expect(res.body.code).toBe("UNAUTHENTICATED");
});
it("accepts token with correct signature, issuer, and audience", async () => {
const nowSeconds = Math.floor(Date.now() / 1000);
const token = signHs256Token(
{
sub: `valid-${Date.now()}`,
iss: "skymoney-api",
aud: "skymoney-web",
iat: nowSeconds,
exp: nowSeconds + 600,
},
JWT_SECRET
);
const res = await request(app.server)
.post("/auth/refresh")
.set("x-csrf-token", csrf)
.set("Cookie", [`csrf=${csrf}`, `session=${token}`]);
expect(res.status).toBe(200);
expect(res.body.ok).toBe(true);
});
});