Security
The CSRF threat model of the form/action loop, and the middleware recipe for ambient-auth apps.
The CSRF threat model of the form/action loop, and the middleware recipe for ambient-auth apps.
The form/action loop relies on the browser SameSite=Lax default for cookie-based authentication: a cross-site POST does not carry the session cookie, so same-site cookies are not a CSRF vector for actions (ADR-0121 §12). First-party sessions are 0.44 scope; until then the framework ships no built-in CSRF token check.
Apps authenticated by ambient credentials — HTTP Basic, mTLS, or cookies set with SameSite=None — cannot rely on the Lax default: the browser attaches those credentials to cross-site requests. Such apps must validate the request origin on every state-changing method.
Drop the middleware below into app/routes/_middleware.ts. A root _middleware.ts default-exports a Hono middleware scoped to /*, in front of every page action and API route. It allows safe methods and same-site Fetch Metadata, and falls back to an Origin allowlist for older browsers.
import type { Context, Next } from 'hono';
// CSRF guard (ADR-0121 §12): the framework's form/action loop assumes
// SameSite=Lax cookies. Apps using ambient authentication (Basic, mTLS,
// SameSite=None cookies) must reject cross-site state-changing requests
// themselves until built-in support lands with 0.44 sessions.
const ALLOWED_ORIGINS = new Set(['https://app.example.com']);
export default async function csrfGuard(c: Context, next: Next) {
const method = c.req.method;
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') {
return next();
}
// Fetch Metadata: same-origin/same-site submissions and user-typed
// navigations are always fine.
const site = c.req.header('sec-fetch-site');
if (site === 'same-origin' || site === 'same-site' || site === 'none') {
return next();
}
// Older browsers without Fetch Metadata: fall back to the Origin header.
const origin = c.req.header('origin');
if (origin && ALLOWED_ORIGINS.has(new URL(origin).origin)) {
return next();
}
return c.text('Forbidden', 403);
}middleware.corsOrigin (createOpenPlugin option) governs cross-origin resource sharing only — it is not a CSRF check. The two compose: CORS for reads, this guard for writes.