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:
145
api/tests/cryptographic-failures.runtime.test.ts
Normal file
145
api/tests/cryptographic-failures.runtime.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user