CSRF Protection
The framework does NOT enforce CSRF protection out of the box. Whether you need it depends on how your action POSTs are authenticated and how your session cookie is configured. This page is a recipe; opt in when you're shipping cookie-authenticated mutations to a real audience.
When you need it
If your app:
- authenticates users via a cookie (signed session, JWT in
Set-Cookie, etc.), and - uses
SameSite=LaxorSameSite=Noneon that cookie, and - accepts file uploads or any other
multipart/form-dataaction
then a malicious page on another origin can submit a top-level <form action="https://yoursite/your-page" enctype="multipart/form-data"> to your page URL with the victim's cookie attached, and your action runs as the victim. That's CSRF.
The JSON path (which useAction uses for any payload that does not contain a File) is already protected by browsers: Content-Type: application/json is not a "safelisted" request header, so browsers force a CORS preflight that a cross-origin attacker page cannot satisfy without your server's explicit allow. (This is a browser-enforced property, not a server-enforced one, but every current browser enforces it.) The multipart path is the gap, because multipart/form-data is safelisted and can be sent cross-origin from a plain <form> without preflight.
You do not need this recipe if any of:
- Your auth cookie is
SameSite=Strict - Your app does not use cookie auth at all (bearer tokens in
Authorization) - Your page URLs are not publicly accessible
- You never accept multipart payloads (convert files to base64 in JSON, or upload via a separate direct route)
The recipe: server-side Origin check
Add a Hono middleware that rejects cross-origin POSTs. Modern browsers always send Origin on cross-origin POSTs, and an attacker page on another origin cannot forge it.
This runs entirely on the server. No client changes, no fetch wrapper, and it covers both useAction requests and native form posts.
// src/api.ts
import { Hono } from 'hono';
const ALLOWED_ORIGIN = 'https://yoursite.example';
const app = new Hono();
app.use('*', async (c, next) => {
if (c.req.method !== 'POST') return next();
const origin = c.req.header('Origin');
if (origin !== ALLOWED_ORIGIN) {
return c.json({ error: 'Cross-origin POST rejected' }, 403);
}
return next();
});
export default app;
This works because the framework mounts your api.ts app ahead of its own handlers and the page renderer. Middleware you register in api.ts therefore runs before the framework's loader RPC and page action handlers: app.use('*', …) is effectively app-wide. Middleware via app.use(…) is always safe: it calls next() and composes ahead of the framework handlers rather than replacing them. What you must not do is register a catch-all route (app.get('*', …), app.all('/*', …), or app.on(…) with '*' as the path) in api.ts: those shadow the framework's handlers, so the build rejects them.
Configure the allowed origin
Hard-code the production origin or read it from an environment variable. Do not derive it from the request (e.g. new URL(c.req.url).origin); behind a CDN or reverse proxy, the request URL the worker sees may use a different scheme or host than the browser sent, and the comparison will silently mis-match or, worse, silently allow.
On Cloudflare Workers, configure ALLOWED_ORIGIN as an environment variable in wrangler.toml and read it from c.env.
If you need to allow multiple origins (e.g. a staging and production host), check against a small allowlist of exact strings. Do not pattern-match.
Verifying it
- From your own site, submit an action; it should succeed.
- From a browser tab on a different origin (e.g.
http://localhost:8080runningpython3 -m http.server), POST a<form enctype="multipart/form-data" action="https://yoursite.example/your-page">with the victim's cookies attached. You should get a 403.
What this does NOT protect against
- Subdomain takeovers. If
a.example.comis compromised and your cookie isDomain=example.com, the attacker is same-origin to you. Use__Host-cookies (noDomainattribute) to scope auth to a single host. - Stored XSS. If an attacker can inject script into your origin, none of this matters; they are you. Set Content Security Policy headers.
- Logged-out cross-origin reads. This recipe is about mutations. GETs to your loaders are public surface; do not put authorization-only data behind GET-with-cookie unless you also know
SameSite=Strictis honored.
For deployment-level controls (CSP, HSTS, frame-ancestors, etc.) see your runtime's documentation. Cloudflare Workers, for example, lets you set response headers from your worker code or via the dashboard.