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 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | /**
* 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 on mutating requests (POST, PUT, DELETE, PATCH).
*
* Defence-in-depth, in order:
* 1. Same-origin check via Origin/Referer header (OWASP-recommended primary
* defence against CSRF — a cross-site forged request from a victim's
* browser always carries the attacker's Origin, never the app's host).
* 2. Double-submit cookie token (the `_csrf` form field or `X-CSRF-Token`
* header must equal the `csrf_token` cookie). Optional — used as a
* belt-and-braces fallback when present.
*
* The Origin check alone is sufficient for modern browsers (all major
* browsers send Origin on cross-origin POSTs). The token check is retained
* because some legacy clients strip Origin/Referer, and because it gives
* forms an explicit "I came from a real page" signal.
*
* A request is accepted iff EITHER the Origin/Referer matches the request
* host OR the token check passes.
*/
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();
}
// Skip CSRF for requests with no session cookie — they are unauthenticated
// and will be redirected to /login (or 404) by downstream auth middleware.
// CSRF only matters for authenticated, cookie-bearing sessions because the
// attack vector is a malicious site tricking a logged-in user's browser.
const sessionCookie = getCookie(c, "session");
if (!sessionCookie) {
return next();
}
// ---- 1) Same-origin check (Origin / Referer header) -----------------
// A genuine same-origin request from our own pages will carry an Origin
// (always for cross-origin POSTs, usually for same-origin too) or a
// Referer that matches the request host. Cross-site forged requests
// either carry the attacker's origin or are stripped — in both cases
// they fail this check.
const host = c.req.header("host");
const origin = c.req.header("origin");
const referer = c.req.header("referer");
let originOk = false;
if (host) {
if (origin) {
try {
originOk = new URL(origin).host === host;
} catch {
originOk = false;
}
} else if (referer) {
try {
originOk = new URL(referer).host === host;
} catch {
originOk = false;
}
}
}
if (originOk) {
return next();
}
// ---- 2) Double-submit cookie token (fallback) ------------------------
const cookieToken = getCookie(c, CSRF_COOKIE);
if (!cookieToken) {
return c.text("CSRF check failed: no Origin/Referer header and no token cookie", 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}" />`;
}
|