feat: implement comprehensive CSRF protection

This commit is contained in:
2025-07-11 18:06:51 +02:00
committed by Kaj Kowalski
parent e7818f5e4f
commit 3e9e75e854
44 changed files with 14964 additions and 6413 deletions

240
tests/unit/csrf.test.ts Normal file
View File

@ -0,0 +1,240 @@
/**
* CSRF Protection Unit Tests
*
* Tests for CSRF token generation, validation, and protection mechanisms.
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { generateCSRFToken, verifyCSRFToken, CSRFProtection, CSRF_CONFIG } from "../../lib/csrf";
// Mock Next.js modules
vi.mock("next/headers", () => ({
cookies: vi.fn(() => ({
get: vi.fn(),
set: vi.fn(),
})),
}));
describe("CSRF Protection", () => {
describe("Token Generation and Verification", () => {
it("should generate a valid CSRF token", () => {
const token = generateCSRFToken();
expect(token).toBeDefined();
expect(typeof token).toBe("string");
expect(token.length).toBeGreaterThan(0);
expect(token.includes(":")).toBe(true);
});
it("should verify a valid CSRF token", () => {
const token = generateCSRFToken();
const isValid = verifyCSRFToken(token);
expect(isValid).toBe(true);
});
it("should reject an invalid CSRF token", () => {
const isValid = verifyCSRFToken("invalid-token");
expect(isValid).toBe(false);
});
it("should reject an empty CSRF token", () => {
const isValid = verifyCSRFToken("");
expect(isValid).toBe(false);
});
it("should reject a malformed CSRF token", () => {
const isValid = verifyCSRFToken("malformed:token:with:extra:parts");
expect(isValid).toBe(false);
});
});
describe("CSRFProtection Class", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should generate token response with correct structure", () => {
const response = CSRFProtection.generateTokenResponse();
expect(response).toHaveProperty("token");
expect(response).toHaveProperty("cookie");
expect(response.cookie).toHaveProperty("name", CSRF_CONFIG.cookieName);
expect(response.cookie).toHaveProperty("value");
expect(response.cookie).toHaveProperty("options");
expect(response.cookie.options).toHaveProperty("httpOnly", true);
expect(response.cookie.options).toHaveProperty("path", "/");
});
it("should validate GET requests without CSRF token", async () => {
const request = new Request("http://localhost/api/test", {
method: "GET",
}) as any;
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(true);
});
it("should validate HEAD requests without CSRF token", async () => {
const request = new Request("http://localhost/api/test", {
method: "HEAD",
}) as any;
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(true);
});
it("should validate OPTIONS requests without CSRF token", async () => {
const request = new Request("http://localhost/api/test", {
method: "OPTIONS",
}) as any;
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(true);
});
it("should reject POST request without CSRF token", async () => {
const request = new Request("http://localhost/api/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ data: "test" }),
}) as any;
// Mock cookies method to return no token
Object.defineProperty(request, "cookies", {
value: {
get: vi.fn(() => undefined),
},
});
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(false);
expect(result.error).toContain("CSRF token missing");
});
it("should validate POST request with valid CSRF token", async () => {
const token = generateCSRFToken();
const request = new Request("http://localhost/api/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
[CSRF_CONFIG.headerName]: token,
},
body: JSON.stringify({ data: "test" }),
}) as any;
// Mock cookies method to return the same token
Object.defineProperty(request, "cookies", {
value: {
get: vi.fn(() => ({ value: token })),
},
});
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(true);
});
it("should reject POST request with mismatched CSRF tokens", async () => {
const headerToken = generateCSRFToken();
const cookieToken = generateCSRFToken();
const request = new Request("http://localhost/api/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
[CSRF_CONFIG.headerName]: headerToken,
},
body: JSON.stringify({ data: "test" }),
}) as any;
// Mock cookies method to return different token
Object.defineProperty(request, "cookies", {
value: {
get: vi.fn(() => ({ value: cookieToken })),
},
});
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(false);
expect(result.error).toContain("mismatch");
});
it("should handle form data CSRF token", async () => {
const token = generateCSRFToken();
const formData = new FormData();
formData.append("csrf_token", token);
formData.append("data", "test");
const request = new Request("http://localhost/api/test", {
method: "POST",
headers: {
"Content-Type": "multipart/form-data",
},
body: formData,
}) as any;
// Mock cookies method to return the same token
Object.defineProperty(request, "cookies", {
value: {
get: vi.fn(() => ({ value: token })),
},
});
// Mock clone method to return a request that can be parsed
Object.defineProperty(request, "clone", {
value: vi.fn(() => ({
formData: async () => formData,
})),
});
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(true);
});
it("should handle JSON body CSRF token", async () => {
const token = generateCSRFToken();
const bodyData = { csrfToken: token, data: "test" };
const request = new Request("http://localhost/api/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(bodyData),
}) as any;
// Mock cookies method to return the same token
Object.defineProperty(request, "cookies", {
value: {
get: vi.fn(() => ({ value: token })),
},
});
// Mock clone method to return a request that can be parsed
Object.defineProperty(request, "clone", {
value: vi.fn(() => ({
json: async () => bodyData,
})),
});
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(true);
});
});
describe("CSRF Configuration", () => {
it("should have correct configuration values", () => {
expect(CSRF_CONFIG.cookieName).toBe("csrf-token");
expect(CSRF_CONFIG.headerName).toBe("x-csrf-token");
expect(CSRF_CONFIG.cookie.httpOnly).toBe(true);
expect(CSRF_CONFIG.cookie.sameSite).toBe("lax");
expect(CSRF_CONFIG.cookie.maxAge).toBe(60 * 60 * 24); // 24 hours
});
it("should use secure cookies in production", () => {
// This would depend on NODE_ENV, which is set in the config
expect(typeof CSRF_CONFIG.cookie.secure).toBe("boolean");
});
});
});