Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

csrf.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

csrf.tsBlame112 lines · 1 contributor
45e31d0Claude1/**
2 * CSRF Protection Middleware
3 *
4 * Generates and validates CSRF tokens for form submissions.
5 * Uses double-submit cookie pattern for stateless CSRF protection.
6 */
7
8import { createMiddleware } from "hono/factory";
9import { getCookie, setCookie } from "hono/cookie";
10
11const CSRF_COOKIE = "csrf_token";
12const CSRF_HEADER = "x-csrf-token";
13const CSRF_FIELD = "_csrf";
14
15function generateToken(): string {
16 const bytes = crypto.getRandomValues(new Uint8Array(32));
17 return Array.from(bytes)
18 .map((b) => b.toString(16).padStart(2, "0"))
19 .join("");
20}
21
22/**
23 * Sets a CSRF cookie on every request if not already present.
24 * Also makes the token available via c.get("csrfToken").
25 */
26export const csrfToken = createMiddleware(async (c, next) => {
27 let token = getCookie(c, CSRF_COOKIE);
28 if (!token) {
29 token = generateToken();
30 setCookie(c, CSRF_COOKIE, token, {
31 httpOnly: false, // JS needs to read it
32 sameSite: "Lax",
33 path: "/",
34 secure: process.env.NODE_ENV === "production",
35 });
36 }
37 c.set("csrfToken", token);
38 return next();
39});
40
41/**
42 * Validates CSRF token on mutating requests (POST, PUT, DELETE, PATCH).
43 * Checks form body field '_csrf' or header 'x-csrf-token' against cookie.
44 */
45export const csrfProtect = createMiddleware(async (c, next) => {
46 const method = c.req.method.toUpperCase();
47 if (["GET", "HEAD", "OPTIONS"].includes(method)) {
48 return next();
49 }
50
51 // Skip CSRF for API routes with Bearer token auth (they have their own auth)
52 const authHeader = c.req.header("Authorization");
53 if (authHeader?.startsWith("Bearer ")) {
54 return next();
55 }
56
59b6fb2Claude57 // Skip CSRF for API routes (they use token auth, not cookies)
45e31d0Claude58 const path = c.req.path;
59b6fb2Claude59 if (path.startsWith("/api/")) {
60 return next();
61 }
62
63 // Skip CSRF for git protocol routes
45e31d0Claude64 if (path.endsWith(".git/git-upload-pack") || path.endsWith(".git/git-receive-pack")) {
65 return next();
66 }
67
68 const cookieToken = getCookie(c, CSRF_COOKIE);
69 if (!cookieToken) {
70 return c.text("CSRF token missing", 403);
71 }
72
73 // Check header first, then form body
74 let submittedToken = c.req.header(CSRF_HEADER);
75 if (!submittedToken) {
76 try {
77 const contentType = c.req.header("content-type") || "";
78 if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
79 const body = await c.req.parseBody();
80 submittedToken = String(body[CSRF_FIELD] || "");
81 }
82 } catch {
83 // Can't parse body — skip
84 }
85 }
86
87 // For JSON API calls from the web UI
88 if (!submittedToken) {
89 try {
90 const contentType = c.req.header("content-type") || "";
91 if (contentType.includes("application/json")) {
92 const body = await c.req.json();
93 submittedToken = body?._csrf;
94 }
95 } catch {
96 // Can't parse JSON — skip
97 }
98 }
99
100 if (!submittedToken || submittedToken !== cookieToken) {
101 return c.text("CSRF token invalid", 403);
102 }
103
104 return next();
105});
106
107/**
108 * Helper to generate a hidden CSRF input field for forms.
109 */
110export function csrfField(token: string): string {
111 return `<input type="hidden" name="${CSRF_FIELD}" value="${token}" />`;
112}