CSRF
The CSRF middleware protects your application from Cross-Site Request Forgery attacks by validating that state-changing requests originate from your own site.
Quick Start
import { MageApp } from "@mage/app";
import { csrf } from "@mage/app/csrf";
const app = new MageApp();
// Enable CSRF protection on all routes
app.use(csrf());
app.post("/api/users", (c) => {
return c.json({ created: true });
});
Deno.serve(app.handler);
How It Works
Validates state-changing requests (POST, PUT, DELETE, PATCH) using
Sec-Fetch-Site and Origin headers. Only checks form submissions (not JSON,
which requires CORS preflight).
Returns 403 if both validation methods fail. See OWASP: CSRF and Fetch Metadata for details.
Options
| Option | Type | Default | Description |
|---|---|---|---|
origin |
string | string[] | IsAllowedOriginHandler |
Request URL origin | Allowed origin(s) for requests. Can be a single string, array of strings, or custom validation function. |
secFetchSite |
SecFetchSite | SecFetchSite[] | IsAllowedSecFetchSiteHandler |
"same-origin" |
Allowed Sec-Fetch-Site header values. Can be a single value, array, or custom validation function. Valid values: "same-origin", "same-site", "none", "cross-site". |
Examples
Basic Protection
The simplest configuration protects against cross-site requests by trusting the default origin validation:
import { MageApp } from "@mage/app";
import { csrf } from "@mage/app/csrf";
const app = new MageApp();
app.use(csrf());
app.post("/api/notes", (c) => {
return c.json({ saved: true });
});
Deno.serve(app.handler);
Allowing Multiple Origins
If your application is served from multiple domains, specify all allowed origins:
app.use(
csrf({
origin: [
"https://example.com",
"https://app.example.com",
"https://staging.example.com",
],
}),
);
Custom Origin Validation
For complex scenarios, use a custom validation function to determine allowed origins dynamically:
app.use(
csrf({
origin: (origin, context) => {
// Allow any subdomain of example.com
if (origin.endsWith(".example.com") || origin === "https://example.com") {
return true;
}
// Allow localhost in development
if (context.env === "development" && origin === "http://localhost:3000") {
return true;
}
return false;
},
}),
);
Allowing Same-Site Requests
Some applications serve content from multiple subdomains that should trust each
other. Use same-site to allow requests between subdomains:
app.use(
csrf({
secFetchSite: "same-site",
}),
);
This allows requests where the origin is on the same registrable domain (e.g.,
api.example.com to app.example.com).
Custom Sec-Fetch-Site Validation
For advanced scenarios, validate the Fetch Metadata header with custom logic:
app.use(
csrf({
secFetchSite: (secFetchSite, context) => {
// Allow same-origin in all environments
if (secFetchSite === "same-origin") {
return true;
}
// Allow same-site cross-origin requests in development
if (context.env === "development" && secFetchSite === "same-site") {
return true;
}
return false;
},
}),
);
Security Considerations
Protects against: Cross-site form submissions, fetch/XHR from other domains
Doesn't protect: Same-site forgery, XSS, subdomain attacks, JSON APIs (use CORS for those)
Best practices:
- Use with
SameSitecookies - Apply to all state-changing endpoints
- List specific origins (avoid wildcards)
- Missing
Originheader = request denied
Notes
- Only checks form content types (not JSON, which requires CORS preflight)
- Request rejected only if both Fetch Metadata and Origin checks fail
- Returns 403 Forbidden for rejected requests
Related
- Middleware System - Learn how middleware works in Mage
- Request and Response - Understanding headers and HTTP methods
- OWASP: Cross-Site Request Forgery (CSRF) - CSRF attack details
- Fetch Metadata Request Headers - MDN guide to Fetch Metadata