♻️ 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
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
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
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
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
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
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