CakePHP CSRF and Security protection for mixed HTML and API routes
Protect browser forms with CakePHP CSRF while keeping stateless API routes token-free. A minimal design uses CsrfProtectionMiddleware after SessionMiddleware, excludes /api/*, and relies on FormHelper for tokens.
09 Dec 2025, 15:49 UTC

The problem is a typical CakePHP app that serves browser forms and a stateless JSON API from the same codebase. Enable CSRF everywhere and API clients break. Disable it everywhere and browser forms become vulnerable to cross-site request forgery. The useful takeaway is to keep CSRF bound to the browser session and explicitly exclude stateless API prefixes, with form tampering protection layered on top of HTML forms only.
Requirements for mixed web and API
Browser-driven state changes must be protected from CSRF. That means POST, PUT, PATCH and DELETE requests that rely on session cookies need a token that is tied to the user session and not guessable by a third site.
Stateless API clients must not require a CSRF token. They authenticate with Authorization headers, JWTs or API keys and do not maintain a session cookie. Requiring a CSRF token for those routes breaks legitimate clients.
CakePHP provides two separate mechanisms. CsrfProtectionMiddleware validates a token for unsafe HTTP methods on browser requests. The Security component provides form tampering protection for HTML forms created with FormHelper, checking that submitted fields match the form definition.
Smallest suitable design
Enable CsrfProtectionMiddleware for web routes only and keep it after SessionMiddleware. Exclude API prefixes from CSRF and protect them with alternative authentication middleware.
In src/Application.php the middleware stack is defined in Application::middleware(). SessionMiddleware must precede CsrfProtectionMiddleware because the token is stored in the session.
// src/Application.php
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
$middlewareQueue
->add(new ErrorHandlerMiddleware(Configure::read('Error.exceptionRenderer')))
->add(new AssetMiddleware())
->add(new RoutingMiddleware($this))
->add(new SessionMiddleware())
->add(new CsrfProtectionMiddleware([
'httponly' => true,
]));
return $middlewareQueue;
}
Route-level exclusion is done in config/routes.php. Scope the API routes and attach a middleware alias that skips CSRF.
// config/routes.php
$routes->scope('/api', function (RouteBuilder $routes) {
$routes->setExtensions(['json']);
$routes->registerMiddleware('csrf_skip', function ($request, $handler) {
if (in_array($request->getMethod(), ['POST','PUT','PATCH','DELETE'])) {
// let API auth middleware handle it, do not enforce CSRF
}
return $handler->handle($request);
});
// API routes here
});
A simpler and common pattern is to keep CsrfProtectionMiddleware globally but configure it to ignore paths. The exact option name varies by CakePHP version. Verify the current version before copying configuration patterns.
For HTML forms, use FormHelper so the hidden _csrfToken field is rendered automatically.
<?= $this->Form->create($entity) ?>
<?= $this->Form->control('email') ?>
<?= $this->Form->button('Save') ?>
<?= $this->Form->end() ?>
For API routes, authentication is handled by token or JWT middleware and CSRF is intentionally disabled for those prefixes.
Trust and data boundaries
Browser session state lives in an encrypted cookie managed by SessionMiddleware. The CSRF token is bound to that session and must never be accepted for routes that rely on Authorization headers.
API credentials live outside the session. Accepting a CSRF token on an API route creates a trust leak where a browser session could be used to influence API behavior, and rejecting CSRF on a web route removes protection.
Keep the boundary explicit: web routes use session + CSRF + Security component tamper checks. API routes use stateless auth and no CSRF.
Operational checks
Verify middleware order. Inspect src/Application.php and confirm SessionMiddleware is added before CsrfProtectionMiddleware. Incorrect order causes token generation failures.
Confirm token rotation on authentication changes. After login or privilege escalation, regenerate the session id and CSRF token to prevent fixation.
Monitor 403 responses for CSRF failures on web routes. A spike indicates missing tokens, cached pages, or misconfigured AJAX.
Log Security component blackhole events rather than silently redirecting. Blackholing can hide application errors if misconfigured.
Verification steps you can run manually:
- Check Application::middleware() for CsrfProtectionMiddleware presence and ordering.
- Review routes configuration to ensure /api/* prefixes are excluded from CSRF and protected by alternative authentication middleware.
- Submit a POST to a web form route without a CSRF token and expect a 403, then submit with a valid token rendered by FormHelper and expect success.
Run these checks with read access to the codebase and ability to issue test requests against a non-production environment. Changing middleware order affects all requests.
Failure modes
Session loss causes valid form submissions to fail. If the session cookie is dropped or expires, the CSRF token will not match and the request is rejected.
Misconfigured route exclusions expose the API to CSRF or block legitimate API calls. Enabling CSRF for stateless API routes breaks clients that do not maintain session cookies.
Cached pages serving stale CSRF tokens cause intermittent 403s. HTML pages with embedded tokens must not be cached by a reverse proxy or CDN.
When to change the design
Multi-domain deployments require SameSite cookie tuning and possibly a different token delivery mechanism.
SPA frontends that consume CakePHP APIs often need a token endpoint to fetch a CSRF token via a same-site cookie, then include it in X-CSRF-Token headers.
If you need double-submit cookie pattern instead of session-bound tokens, the design changes to verify token in cookie and header without server state.
Limitations: middleware names and defaults differ between CakePHP 4 and 5. Configuration moved to Application::middleware() and Security component defaults changed. Assumptions are PHP 8+ runtime and default session handler. Verify current application version before copying configuration patterns.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.