Skip to content

Commit 17b02c4

Browse files
authored
fix(Cookie): validate raw cookie values (#10516)
1 parent c32a6e5 commit 17b02c4

6 files changed

Lines changed: 147 additions & 0 deletions

File tree

system/Cookie/Cookie.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ class Cookie implements ArrayAccess, CloneableCookieInterface
129129
*/
130130
private static string $reservedCharsList = "=,; \t\r\n\v\f()<>@:\\\"/[]?{}";
131131

132+
/**
133+
* @see https://www.php.net/manual/en/function.setrawcookie.php
134+
*/
135+
private static string $reservedValueCharsList = ",; \t\r\n\v\f\0";
136+
132137
/**
133138
* Set the default attributes to a Cookie instance by injecting
134139
* the values from the `CookieConfig` config or an array.
@@ -265,6 +270,7 @@ final public function __construct(string $name, string $value = '', array $optio
265270
$httponly = $options['httponly'];
266271

267272
$this->validateName($name, $raw);
273+
$this->validateValue($value, $raw);
268274
$this->validatePrefix($prefix, $secure, $path, $domain);
269275
$this->validateSameSite($samesite, $secure);
270276

@@ -470,6 +476,8 @@ public function withName(string $name)
470476
*/
471477
public function withValue(string $value)
472478
{
479+
$this->validateValue($value, $this->raw);
480+
473481
$cookie = clone $this;
474482

475483
$cookie->value = $value;
@@ -578,6 +586,7 @@ public function withSameSite(string $samesite)
578586
public function withRaw(bool $raw = true)
579587
{
580588
$this->validateName($this->name, $raw);
589+
$this->validateValue($this->value, $raw);
581590

582591
$cookie = clone $this;
583592

@@ -766,6 +775,21 @@ protected function validateName(string $name, bool $raw): void
766775
}
767776
}
768777

778+
/**
779+
* Validates the cookie value.
780+
*
781+
* If `$raw` is true, values should not contain invalid characters
782+
* as `setrawcookie()` will reject this.
783+
*
784+
* @throws CookieException
785+
*/
786+
protected function validateValue(string $value, bool $raw): void
787+
{
788+
if ($raw && strpbrk($value, self::$reservedValueCharsList) !== false) {
789+
throw CookieException::forInvalidCookieValue();
790+
}
791+
}
792+
769793
/**
770794
* Validates the special prefixes if some attribute requirements are met.
771795
*

system/Cookie/Exceptions/CookieException.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ public static function forEmptyCookieName()
6060
return new static(lang('Cookie.emptyCookieName'));
6161
}
6262

63+
/**
64+
* Thrown when the cookie value contains invalid characters.
65+
*
66+
* @return static
67+
*/
68+
public static function forInvalidCookieValue()
69+
{
70+
return new static(lang('Cookie.invalidCookieValue'));
71+
}
72+
6373
/**
6474
* Thrown when using the `__Secure-` prefix but the `Secure` attribute
6575
* is not set to true.

system/Language/en/Cookie.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
'invalidExpiresTime' => 'Invalid "{0}" type for "Expires" attribute. Expected: string, integer, DateTimeInterface object.',
1717
'invalidExpiresValue' => 'The cookie expiration time is not valid.',
1818
'invalidCookieName' => 'The cookie name "{0}" contains invalid characters.',
19+
'invalidCookieValue' => 'The cookie value contains invalid characters.',
1920
'emptyCookieName' => 'The cookie name cannot be empty.',
2021
'invalidSecurePrefix' => 'Using the "__Secure-" prefix requires setting the "Secure" attribute.',
2122
'invalidHostPrefix' => 'Using the "__Host-" prefix must be set with the "Secure" flag, must not have a "Domain" attribute, and the "Path" is set to "/".',

tests/system/Cookie/CookieTest.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,4 +341,106 @@ public function testCannotUnsetPropertyViaArrayAccess(): void
341341
$cookie = new Cookie('cookie', 'monster');
342342
unset($cookie['path']);
343343
}
344+
345+
#[DataProvider('provideValidationOfRawCookieValue')]
346+
public function testValidationOfRawCookieValue(string $value): void
347+
{
348+
$this->expectException(CookieException::class);
349+
new Cookie('test', $value, ['raw' => true]);
350+
}
351+
352+
/**
353+
* @return iterable<string, array{string}>
354+
*/
355+
public static function provideValidationOfRawCookieValue(): iterable
356+
{
357+
yield 'comma' => ['value,comma'];
358+
359+
yield 'semicolon' => ['value;semicolon'];
360+
361+
yield 'space' => ['value with space'];
362+
363+
yield 'tab' => ["value\twith_tab"];
364+
365+
yield 'carriage return' => ["value\rcarriage"];
366+
367+
yield 'newline' => ["value\nnewline"];
368+
369+
yield 'vertical tab' => ["value\vvertical_tab"];
370+
371+
yield 'form feed' => ["value\fform_feed"];
372+
373+
yield 'null byte' => ["value\0null_byte"];
374+
375+
yield 'CRLF' => ["value\r\nwith_crlf"];
376+
}
377+
378+
#[DataProvider('provideFromHeaderStringValidationOfRawCookieValue')]
379+
public function testFromHeaderStringValidationOfRawCookieValue(string $value): void
380+
{
381+
$this->expectException(CookieException::class);
382+
Cookie::fromHeaderString("test={$value}; Path=/", true);
383+
}
384+
385+
/**
386+
* @return iterable<string, array{string}>
387+
*/
388+
public static function provideFromHeaderStringValidationOfRawCookieValue(): iterable
389+
{
390+
foreach (self::provideValidationOfRawCookieValue() as $name => $case) {
391+
if ($name === 'semicolon') {
392+
continue;
393+
}
394+
395+
yield $name => $case;
396+
}
397+
}
398+
399+
public function testFromHeaderStringWithRawTrue(): void
400+
{
401+
$cookie = Cookie::fromHeaderString('test=valid_raw_value=123; Path=/', true);
402+
403+
$this->assertTrue($cookie->isRaw());
404+
$this->assertSame('valid_raw_value=123', $cookie->getValue());
405+
}
406+
407+
public function testFromHeaderStringWithRawFalseDecodesValue(): void
408+
{
409+
$cookie = Cookie::fromHeaderString('test=value%20with%20space; Path=/', false);
410+
411+
$this->assertFalse($cookie->isRaw());
412+
$this->assertSame('value with space', $cookie->getValue());
413+
}
414+
415+
public function testValidationOfRawCookieValueInWithValue(): void
416+
{
417+
$this->expectException(CookieException::class);
418+
$cookie = new Cookie('test', 'valid_value', ['raw' => true]);
419+
$cookie->withValue("injected\r\nvalue");
420+
}
421+
422+
public function testValidationOfRawCookieValueInWithRaw(): void
423+
{
424+
$this->expectException(CookieException::class);
425+
$cookie = new Cookie('test', "injected\r\nvalue", ['raw' => false]);
426+
$cookie->withRaw(true);
427+
}
428+
429+
public function testValidRawCookieRetainsValueWithoutEncoding(): void
430+
{
431+
$cookie = new Cookie('test', 'valid_raw_value=123', ['raw' => true]);
432+
433+
$this->assertSame('valid_raw_value=123', $cookie->getValue());
434+
$this->assertStringContainsString('test=valid_raw_value=123', (string) $cookie);
435+
}
436+
437+
public function testNonRawCookieSafelyEncodesCRLF(): void
438+
{
439+
$cookie = new Cookie('test', "value\r\nwith_crlf", ['raw' => false]);
440+
$result = (string) $cookie;
441+
442+
$this->assertStringContainsString('%0D%0A', $result);
443+
$this->assertStringNotContainsString("\r", $result);
444+
$this->assertStringNotContainsString("\n", $result);
445+
}
344446
}

user_guide_src/source/changelogs/v4.7.5.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ BREAKING
1818
Message Changes
1919
***************
2020

21+
- Added the ``Cookie.invalidCookieValue`` language string.
22+
2123
*******
2224
Changes
2325
*******
@@ -38,6 +40,7 @@ Bugs Fixed
3840
ANSI color codes in the prompt (e.g., option defaults) are wrapped in readline's non-printing markers under GNU readline so cursor positioning stays accurate.
3941
- **CLIRequest:** Fixed a bug where ``parseCommand()`` could throw a TypeError when ``argv`` is missing.
4042
- **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed.
43+
- **Cookie:** Fixed a bug where ``Cookie`` instances created with ``raw: true`` allowed invalid characters in cookie values rejected by ``setrawcookie()``.
4144
- **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them.
4245
- **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden).
4346
- **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors.

user_guide_src/source/libraries/cookies.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ and `setrawcookie() <https://www.php.net/manual/en/function.setrawcookie.php>`_
9797
will reject cookies with invalid names. Additionally, cookie
9898
names cannot be an empty string.
9999

100+
Validating the Value Attribute
101+
==============================
102+
103+
If setting the ``$raw`` parameter to ``true``, the cookie value will also be validated.
104+
It must not contain control characters, spaces, tabs, or separator characters
105+
(``, ;``) as `setrawcookie() <https://www.php.net/manual/en/function.setrawcookie.php>`_ will reject them.
106+
100107
Validating the Prefix Attribute
101108
===============================
102109

0 commit comments

Comments
 (0)