Add CsrfTokenCookieMiddleware - #100
Conversation
vjik
commented
Aug 28, 2026
| Q | A |
|---|---|
| Is bugfix? | ❌ |
| New feature? | ✔️ |
| Breaks BC? | ❌ |
| Fix #99 |
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #100 +/- ##
===========================================
Coverage 100.00% 100.00%
- Complexity 72 91 +19
===========================================
Files 10 11 +1
Lines 196 244 +48
===========================================
+ Hits 196 244 +48 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds middleware that publishes CSRF tokens in response cookies for SPA cookie-to-header workflows.
Changes:
- Adds configurable CSRF token cookie middleware.
- Adds middleware tests and usage documentation.
- Simplifies null-default handling.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/CsrfTokenCookieMiddleware.php |
Implements cookie publication. |
tests/CsrfTokenCookieMiddlewareTest.php |
Tests cookie behavior. |
README.md |
Documents configuration and usage. |
CHANGELOG.md |
Records the feature. |
src/StubCsrfToken.php |
Simplifies token initialization. |
tests/ConfigTest.php |
Simplifies default parameter loading. |
Suppressed comments (1)
README.md:626
- This copyable configuration is invalid and registers the publisher twice:
// ordoes not make the first entry alternative, and the second definition has no class value. It also places both publisher entries inside the validator, so rejected unsafe requests receive no token cookie. Keep one configured publisher before the validator and specify its class.
CsrfTokenCookieMiddleware::class,
// or
[
'class' => ,
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/CsrfTokenCookieMiddleware.php:63
- Issue #99 explicitly requires
HttpOnlyto be configurable, but this constructor has no such option and the header builder can never emit the attribute. Add a boolean option (defaulting tofalsefor the cookie-to-header use case), appendHttpOnlywhen enabled, and cover/document both settings.
public function __construct(
CsrfTokenInterface $token,
string $cookieName = self::COOKIE_NAME,
string $path = '/',
?string $domain = null,
bool $secure = true,
?string $sameSite = self::SAME_SITE_LAX
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/CsrfTokenCookieMiddleware.php:45
- This whitelist contains literal spaces, so names such as
X SRFpass even though whitespace is forbidden in cookie names. The$anchor also permits a final LF, which can produce an invalid or injectableSet-Cookieheader. Remove the formatting spaces and use an absolute end anchor.
private const PATTERN_COOKIE_NAME = '/^[a-zA-Z0-9!#$%&\' * +\- .^_`|~]+$/';
src/CsrfTokenCookieMiddleware.php:78
- Issue #99 explicitly requires a configurable
HttpOnlyoption, but the constructor has no such parameter and the builder never emitsHttpOnly. Add a boolean option (defaulting tofalsefor the JavaScript-readable use case) and serialize, test, and document it so the requested API is complete.
?string $sameSite = self::SAME_SITE_LAX
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/CsrfTokenCookieMiddleware.php:98
- This check only rejects control characters and semicolons; it accepts values such as an empty string,
https://example.com, ornot a domain. Those values are not valid RFC 6265 domain-values, so they are serialized into aSet-Cookieheader that browsers reject instead of triggering the documentedInvalidArgumentException. Validate the domain grammar (while accounting for supported host forms) and add invalid-domain cases.
if ($domain !== null && preg_match(self::PATTERN_INVALID_ATTRIBUTE_VALUE, $domain)) {
throw new InvalidArgumentException(
sprintf('The cookie domain "%s" contains invalid characters.', $domain),
);
}
src/CsrfTokenCookieMiddleware.php:80
- Issue #99 explicitly requires
HttpOnlyto be configurable, but the public constructor has no such option and the header builder can never emit the attribute. Add anhttpOnlyoption (defaulting tofalsefor SPA compatibility), serializeHttpOnlywhen enabled, and cover/document both settings.
bool $secure = true,
?string $sameSite = self::SAME_SITE_LAX
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
src/CsrfTokenCookieMiddleware.php:31
- The linked double-submit-cookie pattern validates a value derived from the request cookie, while this middleware explicitly never treats that cookie as proof and instead submits the token through a custom header. Pointing to the custom-request-header guidance avoids documenting this as a different CSRF pattern.
* @link https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#alternative-using-a-double-submit-cookie-pattern
src/CsrfTokenCookieMiddleware.php:76
- Issue #99 explicitly requires a configurable
HttpOnlyoption, but this API always emits a JavaScript-readable cookie and provides no way to enable the attribute. Add a boolean option (defaulting tofalse), emitHttpOnlyconditionally, and cover/document both settings.
string $cookieName = self::COOKIE_NAME,
string $path = '/',
?string $domain = null,
bool $secure = true,
?string $sameSite = self::SAME_SITE_LAX
src/CsrfTokenCookieMiddleware.php:78
- This validation still accepts an empty name and cookie separators such as
=or spaces. For example, configuringfoo=baremitsfoo=bar=<token>, which creates a cookie namedfoorather thanfoo=bar. Validate the name against the RFC cookie-name token grammar, not only header-injection characters.
if (preg_match(self::PATTERN_HEADER_INJECTION, $cookieName)) {
|
|
||
| $parts[] = 'Path=' . $this->path; | ||
|
|
||
| if ($this->secure) { |
There was a problem hiding this comment.
It would be great if we could use autodetect in the secure option, so the user wouldn't have to configure anything.
$secure = $this->secure ?? strtolower($request->getUri()->getScheme()) === 'https';This is how it works both in development with localhost and for production environments.
| * cookie name, path or domain inject extra attributes or split the response header. Whether the values are | ||
| * otherwise well-formed is left to the caller. | ||
| */ | ||
| private const PATTERN_HEADER_INJECTION = '/[\x00-\x1F\x7F\x3B]/'; |
There was a problem hiding this comment.
= only matters for cookieName, but this pattern also validates path and domain, where = is. This class doesn't aim to fully validate cookie attributes against the RFC — only to keep the response header safe from
injection. So = doesn't belong here.
| { | ||
| $response = $handler->handle($request); | ||
|
|
||
| return $response->withAddedHeader(Header::SET_COOKIE, $this->buildCookieHeaderValue()); |
There was a problem hiding this comment.
Likely needs Cache-Control/Vary for GET to mark the response as non-cacheable.
…hen not set Make the `$secure` constructor parameter `?bool` defaulting to `null`. When `null`, the `Secure` cookie attribute is resolved per request from the request URI scheme (added when the scheme is `https`). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D9c67jXQWZ9kKSbzoV747f
By default the middleware now sends `Cache-Control: no-store` for responses that publish the token and carry no `Cache-Control` of their own, keeping the token out of shared and private caches. The `$cacheControl` constructor argument accepts `true` (default), `false` to leave the header untouched, or a string to set an explicit value. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D9c67jXQWZ9kKSbzoV747f
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
src/CsrfTokenCookieMiddleware.php:95
- Issue #99 explicitly requires the cookie's
HttpOnlyoption to be configurable, but the new constructor exposes no such option and the generated header can never includeHttpOnly. Add a boolean constructor option (defaulting tofalsefor the JavaScript-readable use case) and apply it when building the cookie header.
?string $domain = null,
?bool $secure = null,
?string $sameSite = self::SAME_SITE_LAX,
$cacheControl = true
When SameSite=None was combined with the default secure=null and the request came over HTTP, secure resolved to false and the cookie was emitted without the Secure attribute, which browsers reject. An explicit secure value still wins; when it is null, SameSite=None now resolves secure to true, otherwise the request URI scheme decides. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D9c67jXQWZ9kKSbzoV747f
The middleware makes a response user-specific by publishing a per-user token, so a previously cacheable `Cache-Control` (for example `public`) is no longer safe and a shared cache could replay one user's token to another. With `$cacheControl === true` or a string, `Cache-Control` is now replaced rather than kept when the response already has one; callers that manage caching themselves can still pass `false`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D9c67jXQWZ9kKSbzoV747f
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/CsrfTokenCookieMiddleware.php:96
- Issue #99 explicitly requires the response-cookie feature to support a configurable
HttpOnlyoption, but the constructor exposes no such setting and cookie serialization never emits the attribute. Add an option (defaulting tofalseso the SPA use case remains JavaScript-readable), serialize it, and document/test both values.
?bool $secure = null,
?string $sameSite = self::SAME_SITE_LAX,
$cacheControl = true