security/platform: close audit findings #19–#26

Tests, CSP nonce middleware, SSRF guard, perf-route hardening,
Docker env isolation, migration runbook, RBAC E2E coverage.

Tickets resolved:
- #19: MfaSetup.test.ts — static source tests confirming local QR rendering
- #20: ssrf-guard.test.ts (16 tests) + webhook-procedure-support mock fix
- #21: /api/perf route.test.ts (5 tests) — header-only auth, fail-closed
- #22: middleware.ts (nonce-based CSP) + middleware.test.ts (6 tests);
       layout.tsx async + nonce prop; CSP removed from next.config.ts
- #23: Active-session registry enforcement verified (already in codebase)
- #24: docker-compose.yml REDIS_URL hardcoded (no host-env substitution)
- #25: docker-compose.yml REDIS_URL + docs/developer-runbook.md created
- #26: e2e/dev-system/rbac-data-access.spec.ts (12 tests, 3 roles × 4 procedures)

Quality gates: tsc clean, api 1447/1447, web 189/189 passing.
Turbo concurrency capped at 2 (package.json) to prevent OOM under
parallel test runs.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-04-01 22:14:20 +02:00
parent 4901bc878b
commit bfdf0a82da
15 changed files with 1013 additions and 23 deletions
+88
View File
@@ -0,0 +1,88 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@capakraken/api/sse", () => ({
eventBus: { subscriberCount: 0 },
}));
// Lazy import so we can stub env before the module-level code runs.
const importRoute = () => import("./route.js");
describe("GET /api/perf — security hardening", () => {
const ORIGINAL_SECRET = process.env["CRON_SECRET"];
beforeEach(() => {
vi.resetModules();
});
afterEach(() => {
if (ORIGINAL_SECRET === undefined) {
delete process.env["CRON_SECRET"];
} else {
process.env["CRON_SECRET"] = ORIGINAL_SECRET;
}
});
it("returns 200 with metrics for an authorised request via Authorization header", async () => {
process.env["CRON_SECRET"] = "test-secret-abc";
const { GET } = await importRoute();
const request = new Request("http://localhost/api/perf", {
headers: { Authorization: "Bearer test-secret-abc" },
});
const response = await GET(request);
expect(response.status).toBe(200);
const body = await response.json() as { timestamp: string; uptime: unknown; memory: unknown };
expect(typeof body.timestamp).toBe("string");
expect(body.uptime).toBeDefined();
expect(body.memory).toBeDefined();
});
it("returns 401 when no Authorization header is provided", async () => {
process.env["CRON_SECRET"] = "test-secret-abc";
const { GET } = await importRoute();
const request = new Request("http://localhost/api/perf");
const response = await GET(request);
expect(response.status).toBe(401);
});
it("returns 401 when the Authorization header contains a wrong secret", async () => {
process.env["CRON_SECRET"] = "test-secret-abc";
const { GET } = await importRoute();
const request = new Request("http://localhost/api/perf", {
headers: { Authorization: "Bearer wrong-secret" },
});
const response = await GET(request);
expect(response.status).toBe(401);
});
it("returns 401 for a query-param token — query-string auth is not supported", async () => {
process.env["CRON_SECRET"] = "test-secret-abc";
const { GET } = await importRoute();
// Pass secret as query param only — no Authorization header
const request = new Request("http://localhost/api/perf?token=test-secret-abc");
const response = await GET(request);
// The endpoint ignores query params entirely; without a valid header it must reject.
expect(response.status).toBe(401);
});
it("returns 401 and leaks no metrics when CRON_SECRET is not configured (fail-closed)", async () => {
delete process.env["CRON_SECRET"];
const { GET } = await importRoute();
// Even a request that would otherwise be valid must be rejected.
const request = new Request("http://localhost/api/perf", {
headers: { Authorization: "Bearer anything" },
});
const response = await GET(request);
expect(response.status).toBe(401);
const body = await response.json() as { error?: string; timestamp?: string; memory?: unknown };
expect(body.timestamp).toBeUndefined();
expect(body.memory).toBeUndefined();
});
});
+3 -1
View File
@@ -7,7 +7,9 @@ export const runtime = "nodejs";
/**
* GET /api/perf — Runtime performance metrics.
*
* Protected by CRON_SECRET header or query param.
* Protected by CRON_SECRET via `Authorization: Bearer <secret>` header only.
* Query-string authentication is not supported (secrets must not appear in URLs).
* Fails closed (401) when CRON_SECRET is not configured in the environment.
* Returns Node.js memory usage, process uptime, and SSE connection count.
*/
export function GET(request: Request) {