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