PSR-15 CSRF protection and token managers for PHP 8.4+.
The package combines a session-bound CSRF token with browser request-context checks. Stateful applications should prefer a synchronizer token or a session-bound HMAC token. SameSite cookies and Fetch Metadata are defense-in-depth controls, not replacements for a CSRF token.
composer require componenta/http-csrf-middlewareThe package has no config provider. Construct the middleware and a PSR-17 response factory explicitly.
| Manager | Use |
|---|---|
HmacCsrfTokenManager |
Stateless token signed with a server secret and bound to trusted session state. |
SessionCsrfTokenManager |
256-bit random synchronizer token stored in a native PHP session. |
CookieCsrfTokenManager |
Legacy double-submit mode only; deprecated for new integrations. |
For Auth 3 browser sessions use AuthSessionCsrfTokenManager / AuthSessionCsrfMiddleware from componenta/auth-session-http. They bind the token to the authenticated session UUID and credential generation.
$tokens = new HmacCsrfTokenManager(
secretKey: $csrfKey,
ttl: 3600,
sessionBinding: $authenticatedSessionId . ':' . $credentialGeneration,
);
$middleware = new CsrfMiddleware(
tokenManager: $tokens,
responseFactory: $responseFactory,
);Requirements:
- use a cryptographically random server key of at least 32 bytes;
- derive
sessionBindingfrom trusted server-side session state; - change the binding on login/session rotation and credential generations that must invalidate CSRF tokens;
- never derive the binding from submitted request data;
- create the manager for the current request/session context, not as a cross-user singleton.
Tokens use v2.nonce.timestamp.mac. The binding is covered by the MAC but is not disclosed in the token. Validation uses constant-time comparison, rejects malformed/legacy tokens, enforces TTL, and permits at most 30 seconds of future clock skew.
Unsafe methods require all enabled layers to pass:
- Fetch Metadata policy;
- Origin/Referer verification;
- CSRF token verification.
Safe RFC methods GET, HEAD, OPTIONS, and TRACE do not require a submitted token and receive the active/generated CSRF token as a request attribute. HTTP method tokens are case-sensitive: lowercase lookalikes such as get are treated as custom unsafe methods and must pass CSRF validation.
This classification follows RFC 9110 semantics. Application routes using a safe method must not perform requested state-changing actions; a state-changing GET is an HTTP contract violation and is outside the protection boundary of this middleware. OWASP likewise recommends not using GET for state changes.
checkFetchMetadata defaults to true.
Unsafe requests with:
Sec-Fetch-Site: cross-siteare rejected before token validation unless their Origin is listed explicitly in trustedOrigins.
Recognized values are:
same-origin;same-site;cross-site;none.
Unknown future Sec-Fetch-Site values are ignored for forward compatibility; the request still has to pass the configured Origin/Referer and CSRF-token checks.
Fetch Metadata is browser-controlled defense in depth. It does not replace the CSRF token and can be absent on legacy/non-browser clients. Successful unsafe responses add Vary: Origin, Sec-Fetch-Site for the checks that are enabled, while preserving Vary: *.
checkOrigin defaults to true.
The middleware prefers Origin; when it is absent it falls back to Referer. Both are compared as exact origins including scheme, host, and effective port.
The following fail closed:
- malformed
Origin/Referer; Origin: null;- target request URI without a trustworthy HTTP(S) scheme/host;
- both
OriginandReferermissing.
For a legacy/non-browser integration that cannot send either source header, the compatibility escape hatch is explicit:
new CsrfMiddleware(
tokenManager: $tokens,
responseFactory: $responseFactory,
allowMissingOrigin: true,
);The CSRF token is still mandatory on unsafe methods.
When the externally visible application origin is fixed, prefer an explicit trusted server-side target origin:
new CsrfMiddleware(
tokenManager: $tokens,
responseFactory: $responseFactory,
targetOrigin: 'https://shop.example.com',
);targetOrigin must be an exact non-opaque HTTP(S) origin. When configured, Origin/Referer checks compare against it instead of deriving the target from the incoming request URI. This follows OWASP guidance to use a trusted configured target origin where possible and avoids making CSRF trust depend on an attacker-influenced Host value or proxy reconstruction.
If the public origin is dynamic, do not read X-Forwarded-* directly inside CSRF middleware. Normalize the effective request URI first with componenta/http-trusted-proxy-middleware:
TrustedProxyMiddleware
-> CsrfMiddleware
-> application
Only configured trusted proxies may affect scheme/host/port. With no targetOrigin, the CSRF middleware compares the source origin against that normalized PSR-7 URI.
trustedOrigins is an explicit exact allowlist:
trustedOrigins: [
'https://app.example.com',
'https://admin.example.com:8443',
]Entries must be exact HTTP(S) origins. Paths, queries, fragments, userinfo, opaque null, and malformed values are rejected during construction.
No suffix/subdomain wildcard matching is performed.
Header submission is preferred:
X-CSRF-Token: <token>HTML forms may submit:
_csrf_token=<token>
If the configured header is present, it has precedence over the body. An empty, invalid, or multiply-specified token header cannot fall back to a valid body token.
CSRF tokens must not be placed in URLs or logs.
excludedPaths is intended only for endpoints protected by a different trust mechanism, such as signed webhooks.
Matching is path-segment aware:
excludedPaths: ['/webhook']matches:
/webhook;/webhook/provider;
but not:
/webhook-admin.
Empty, root-only, query-bearing, fragment-bearing, and path-confusing exclusions are rejected. Exclusion matching fails closed for ambiguous request paths such as literal dot segments, backslashes, NULs, encoded path separators/dot segments, and their repeatedly percent-encoded forms. This prevents a proxy/router normalization mismatch from turning a webhook-style exemption into a CSRF bypass. Excluded requests that pass these checks bypass CSRF entirely and do not receive token attributes.
Rejected requests receive 403 plus:
Cache-Control: no-store
Pragma: no-cacheDetailed reasons are not exposed by default.
For local diagnostics only:
debugFailureHeader: trueadds X-CSRF-Failure.
SessionCsrfTokenManager stores a 32-byte random token as 64 lowercase hex characters. Stored session values that do not match this format are treated as unavailable.
The manager starts the native PHP session when needed and throws if session storage cannot be opened.
Session identifier rotation/fixation prevention remains the responsibility of the authentication/session layer.
CookieCsrfTokenManager is deprecated for new code. It is an unsigned double-submit pattern and should be replaced with the session or HMAC managers.
To reduce cookie-injection risk, its cookie is now constrained to __Host- semantics:
- cookie name must start with
__Host-; Secure=true;Path=/;- no
Domainattribute.
The default name is __Host-csrf_token.
These constraints follow the current cookie standard, RFC 10025 (July 2026), which obsoletes RFC 6265 and defines __Host- as Secure, host-only, and Path=/. SameSite remains defense in depth; the CSRF token is still required.
GitHub Actions verifies:
- PHP 8.4 and 8.5;
- lowest and highest supported dependencies;
composer validate --strict;composer audit;- PHPStan level max for
srcandtests; - PHPUnit security regression tests.
- OWASP Cross-Site Request Forgery Prevention Cheat Sheet;
- RFC 9110 safe-method and HTTP semantics;
- RFC 6454 origin semantics;
- RFC 10025 cookie security and
__Host-prefix semantics; - Fetch Metadata request-header guidance;
- OWASP path-confusion/canonicalization guidance for security-relevant path matching.