♻️ refactor: update csrf.ts
swadhinbiswas committed 3 months ago
8674de0
1 parent 5c45620
commit 8674de0
1 file changed
+33 -33
Showing 1 changed file+33-33
▼src/middleware/csrf.ts
+33-33
| @@ -26,9 +26,16 @@ function getCsrfTokenFromCookie(request: Request): string | null { | ||
26 | 26 | |
27 | 27 | const cookies = cookieHeader.split(";").map((c) => c.trim()); |
28 | 28 | for (const cookie of cookies) { |
29 | - const [name, value] = cookie.split("="); | |
29 | + const eqIndex = cookie.indexOf("="); | |
30 | + if (eqIndex === -1) continue; | |
31 | + const name = cookie.substring(0, eqIndex).trim(); | |
32 | + const value = cookie.substring(eqIndex + 1).trim(); | |
30 | 33 | if (name === CSRF_COOKIE_NAME) { |
31 | - return decodeURIComponent(value); | |
34 | + try { | |
35 | + return decodeURIComponent(value); | |
36 | + } catch { | |
37 | + return value; | |
38 | + } | |
32 | 39 | } |
33 | 40 | } |
34 | 41 | return null; |
| @@ -36,20 +43,35 @@ function getCsrfTokenFromCookie(request: Request): string | null { | ||
36 | 43 | |
37 | 44 | /** |
38 | 45 | * Extract CSRF token from request headers or body |
46 | + * Uses request.clone() to avoid consuming the body stream | |
39 | 47 | */ |
40 | -function getCsrfTokenFromRequest(request: Request): string | null { | |
48 | +async function getCsrfTokenFromRequest(request: Request): Promise<string | null> { | |
41 | 49 | // Check header first |
42 | 50 | const headerToken = request.headers.get(CSRF_HEADER_NAME); |
43 | 51 | if (headerToken) return headerToken; |
44 | 52 | |
45 | - // For form submissions, check _csrf form field (handled by consumer) | |
53 | + // For form submissions, check _csrf form field | |
54 | + // Clone the request so we don't consume the body stream | |
55 | + const contentType = request.headers.get("content-type") || ""; | |
56 | + if (contentType.includes("application/x-www-form-urlencoded")) { | |
57 | + try { | |
58 | + const cloned = request.clone(); | |
59 | + const body = await cloned.text(); | |
60 | + const params = new URLSearchParams(body); | |
61 | + const formToken = params.get("_csrf"); | |
62 | + if (formToken) return formToken; | |
63 | + } catch { | |
64 | + // Ignore errors parsing body | |
65 | + } | |
66 | + } | |
67 | + | |
46 | 68 | return null; |
47 | 69 | } |
48 | 70 | |
49 | 71 | /** |
50 | 72 | * Validate CSRF token |
51 | 73 | */ |
52 | -export function validateCsrfToken(request: Request): boolean { | |
74 | +export async function validateCsrfToken(request: Request): Promise<boolean> { | |
53 | 75 | // GET, HEAD, OPTIONS are safe methods - no CSRF protection needed |
54 | 76 | if (["GET", "HEAD", "OPTIONS"].includes(request.method)) { |
55 | 77 | return true; |
| @@ -70,7 +92,7 @@ export function validateCsrfToken(request: Request): boolean { | ||
70 | 92 | } |
71 | 93 | |
72 | 94 | const cookieToken = getCsrfTokenFromCookie(request); |
73 | - const requestToken = getCsrfTokenFromRequest(request); | |
95 | + const requestToken = await getCsrfTokenFromRequest(request); | |
74 | 96 | |
75 | 97 | // Both must exist and match |
76 | 98 | if (!cookieToken || !requestToken) { |
| @@ -102,7 +124,9 @@ function timingSafeEqual(a: string, b: string): boolean { | ||
102 | 124 | */ |
103 | 125 | export function createCsrfCookie(token: string, secure: boolean = false): string { |
104 | 126 | const maxAge = 86400; // 24 hours |
105 | - const sameSite = "Strict"; | |
127 | + // Use Lax instead of Strict — Strict cookies are not sent on same-site | |
128 | + // form POSTs in some browser configurations, causing CSRF failures | |
129 | + const sameSite = "Lax"; | |
106 | 130 | |
107 | 131 | return [ |
108 | 132 | `${CSRF_COOKIE_NAME}=${encodeURIComponent(token)}`, |
| @@ -122,7 +146,8 @@ export function createCsrfCookie(token: string, secure: boolean = false): string | ||
122 | 146 | export async function applyCsrfProtection( |
123 | 147 | request: Request |
124 | 148 | ): Promise<Response | null> { |
125 | - if (!validateCsrfToken(request)) { | |
149 | + const isValid = await validateCsrfToken(request); | |
150 | + if (!isValid) { | |
126 | 151 | return new Response( |
127 | 152 | JSON.stringify({ |
128 | 153 | error: "CSRF token validation failed", |
| @@ -156,28 +181,3 @@ export function getCsrfToken(request: Request): { token: string; cookie: string | ||
156 | 181 | |
157 | 182 | return { token, cookie }; |
158 | 183 | } |
159 | - | |
160 | -/** | |
161 | - * Example usage in Astro component: | |
162 | - * | |
163 | - * --- | |
164 | - * import { getCsrfToken } from "@/middleware/csrf"; | |
165 | - * const { token } = getCsrfToken(Astro.request); | |
166 | - * Astro.response.headers.set("Set-Cookie", getCsrfToken(Astro.request).cookie); | |
167 | - * --- | |
168 | - * | |
169 | - * <form method="POST"> | |
170 | - * <input type="hidden" name="_csrf" value={token} /> | |
171 | - * <!-- rest of form --> | |
172 | - * </form> | |
173 | - * | |
174 | - * Or for fetch requests: | |
175 | - * fetch("/api/endpoint", { | |
176 | - * method: "POST", | |
177 | - * headers: { | |
178 | - * "X-CSRF-Token": token, | |
179 | - * "Content-Type": "application/json" | |
180 | - * }, | |
181 | - * body: JSON.stringify(data) | |
182 | - * }) | |
183 | - */ | |
184 | 184 | |