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.tsBlame107 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
57 // Skip CSRF for git protocol routes
58 const path = c.req.path;
59 if (path.endsWith(".git/git-upload-pack") || path.endsWith(".git/git-receive-pack")) {
60 return next();
61 }
62
63 const cookieToken = getCookie(c, CSRF_COOKIE);
64 if (!cookieToken) {
65 return c.text("CSRF token missing", 403);
66 }
67
68 // Check header first, then form body
69 let submittedToken = c.req.header(CSRF_HEADER);
70 if (!submittedToken) {
71 try {
72 const contentType = c.req.header("content-type") || "";
73 if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
74 const body = await c.req.parseBody();
75 submittedToken = String(body[CSRF_FIELD] || "");
76 }
77 } catch {
78 // Can't parse body — skip
79 }
80 }
81
82 // For JSON API calls from the web UI
83 if (!submittedToken) {
84 try {
85 const contentType = c.req.header("content-type") || "";
86 if (contentType.includes("application/json")) {
87 const body = await c.req.json();
88 submittedToken = body?._csrf;
89 }
90 } catch {
91 // Can't parse JSON — skip
92 }
93 }
94
95 if (!submittedToken || submittedToken !== cookieToken) {
96 return c.text("CSRF token invalid", 403);
97 }
98
99 return next();
100});
101
102/**
103 * Helper to generate a hidden CSRF input field for forms.
104 */
105export function csrfField(token: string): string {
106 return `<input type="hidden" name="${CSRF_FIELD}" value="${token}" />`;
107}