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.tsBlame121 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
699e5c7Claude68 // Skip CSRF for requests with no session cookie — they are unauthenticated
69 // and will be redirected to /login (or 404) by downstream auth middleware.
70 // CSRF only matters for authenticated, cookie-bearing sessions because the
71 // attack vector is a malicious site tricking a logged-in user's browser.
72 const sessionCookie = getCookie(c, "session");
73 if (!sessionCookie) {
74 return next();
75 }
76
45e31d0Claude77 const cookieToken = getCookie(c, CSRF_COOKIE);
78 if (!cookieToken) {
79 return c.text("CSRF token missing", 403);
80 }
81
82 // Check header first, then form body
83 let submittedToken = c.req.header(CSRF_HEADER);
84 if (!submittedToken) {
85 try {
86 const contentType = c.req.header("content-type") || "";
87 if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
88 const body = await c.req.parseBody();
89 submittedToken = String(body[CSRF_FIELD] || "");
90 }
91 } catch {
92 // Can't parse body — skip
93 }
94 }
95
96 // For JSON API calls from the web UI
97 if (!submittedToken) {
98 try {
99 const contentType = c.req.header("content-type") || "";
100 if (contentType.includes("application/json")) {
101 const body = await c.req.json();
102 submittedToken = body?._csrf;
103 }
104 } catch {
105 // Can't parse JSON — skip
106 }
107 }
108
109 if (!submittedToken || submittedToken !== cookieToken) {
110 return c.text("CSRF token invalid", 403);
111 }
112
113 return next();
114});
115
116/**
117 * Helper to generate a hidden CSRF input field for forms.
118 */
119export function csrfField(token: string): string {
120 return `<input type="hidden" name="${CSRF_FIELD}" value="${token}" />`;
121}