From 2f6ad5a0a19ab2ce92a7bed39cefdd5fe1ee9c86 Mon Sep 17 00:00:00 2001 From: anshul23102 Date: Wed, 3 Jun 2026 18:45:16 +0530 Subject: [PATCH] fix(csrf): add per-session CSRF token to state-mutating auth requests POST /api/auth/login was submitted with no CSRF protection. An attacker could craft a page that silently submits the form from a third-party origin using the victim's browser session, since the browser automatically attaches session cookies to cross-origin requests. Add a csrf.ts utility that generates a cryptographically random 32-byte token via window.crypto.getRandomValues(), stores it in sessionStorage for the lifetime of the tab, and exposes csrfHeaders() for use with axios. Apply csrfHeaders() to the login POST request. The backend must validate the X-CSRF-Token header against the server-side session to complete the protection chain. Closes #689 --- src/pages/Login/Login.tsx | 5 ++++- src/utils/csrf.ts | 40 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/utils/csrf.ts diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index 92b7073e..6a88f332 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -3,6 +3,7 @@ import axios from "axios"; import { useNavigate, Link } from "react-router-dom"; import { ThemeContext } from "../../context/ThemeContext"; import type { ThemeContextType } from "../../context/ThemeContext"; +import { csrfHeaders } from "../../utils/csrf"; const backendUrl = import.meta.env.VITE_BACKEND_URL; @@ -30,7 +31,9 @@ const Login: React.FC = () => { setIsLoading(true); try { - const response = await axios.post(`${backendUrl}/api/auth/login`, formData); + const response = await axios.post(`${backendUrl}/api/auth/login`, formData, { + headers: csrfHeaders(), + }); setMessage(response.data.message); if (response.data.message === 'Login successful') { diff --git a/src/utils/csrf.ts b/src/utils/csrf.ts new file mode 100644 index 00000000..a0d8da26 --- /dev/null +++ b/src/utils/csrf.ts @@ -0,0 +1,40 @@ +/** + * Client-side CSRF token utilities. + * + * Generates a per-session random token stored in sessionStorage and provides + * a helper that attaches it as the X-CSRF-Token header on state-mutating + * requests. The backend must validate this header against the value it set + * in the session. Without such a header, a cross-site form submission carries + * no distinguishing marker and cannot be rejected. + */ + +const CSRF_TOKEN_KEY = 'csrf_token'; + +/** + * Return the existing session CSRF token, creating one if absent. + * The token is a 32-byte hex string generated via Web Crypto. + */ +export function getCsrfToken(): string { + const existing = sessionStorage.getItem(CSRF_TOKEN_KEY); + if (existing) return existing; + + const bytes = new Uint8Array(32); + window.crypto.getRandomValues(bytes); + const token = Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + + sessionStorage.setItem(CSRF_TOKEN_KEY, token); + return token; +} + +/** + * Return an axios-compatible headers object that includes the CSRF token. + * Spread this into the headers of any POST / PUT / PATCH / DELETE request. + * + * @example + * axios.post(url, body, { headers: csrfHeaders() }); + */ +export function csrfHeaders(): Record { + return { 'X-CSRF-Token': getCsrfToken() }; +}