8000 [HttpFoundation] Cookies Having Independent Partitioned State (CHIPS) by fabricecw · Pull Request #52002 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[HttpFoundation] Cookies Having Independent Partitioned State (CHIPS) #52002

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
10000
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpFoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CHANGELOG
* Make `HeaderBag::getDate()`, `Response::getDate()`, `getExpires()` and `getLastModified()` return a `DateTimeImmutable`
* Support root-level `Generator` in `StreamedJsonResponse`
* Add `UriSigner` from the HttpKernel component
* Add `partitioned` flag to `Cookie` (CHIPS Cookie)

6.3
---
Expand Down
41 changes: 35 additions & 6 deletions src/Symfony/Component/HttpFoundation/Cookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class Cookie

private bool $raw;
private ?string $sameSite = null;
private bool $partitioned = false;
private bool $secureDefault = false;

private const RESERVED_CHARS_LIST = "=,; \t\r\n\v\f";
Expand All @@ -51,6 +52,7 @@ public static function fromString(string $cookie, bool $decode = false): static
'httponly' => false,
'raw' => !$decode,
'samesite' => null,
'partitioned' => false,
];

$parts = HeaderUtils::split($cookie, ';=');
Expand All @@ -66,17 +68,20 @@ public static function fromString(string $cookie, bool $decode = false): static
$data['expires'] = time() + (int) $data['max-age'];
}

return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']);
return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite'], $data['partitioned']);
}

/**
* @see self::__construct
*
* @param self::SAMESITE_*|''|null $sameSite
* @param bool $partitioned
*/
public static function create(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX): self
public static function create(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX /* , bool $partitioned = false */): self
{
return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite);
$partitioned = 9 < \func_num_args() ? func_get_arg(9) : false;

return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite, $partitioned);
}

/**
Expand All @@ -92,7 +97,7 @@ public static function create(string $name, string $value = null, int|string|\Da
*
* @throws \InvalidArgumentException
*/
public function __construct(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX)
public function __construct(string $name, string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX, bool $partitioned = false)
{
// from PHP source code
if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) {
Expand All @@ -112,6 +117,7 @@ public function __construct(string $name, string $value = null, int|string|\Date
$this->httpOnly = $httpOnly;
$this->raw = $raw;
$this->sameSite = $this->withSameSite($sameSite)->sameSite;
$this->partitioned = $partitioned;
}

/**
Expand Down Expand Up @@ -237,6 +243,17 @@ public function withSameSite(?string $sameSite): static
return $cookie;
}

/**
* Creates a cookie copy that is tied to the top-level site in cross-site context.
*/
public function withPartitioned(bool $partitioned = true): static
{
$cookie = clone $this;
$cookie->partitioned = $partitioned;

return $cookie;
}

/**
* Returns the cookie as a string.
*/
Expand Down Expand Up @@ -268,18 +285,22 @@ public function __toString(): string
$str .= '; domain='.$this->getDomain();
}

if (true === $this->isSecure()) {
if ($this->isSecure()) {
$str .= '; secure';
}

if (true === $this->isHttpOnly()) {
if ($this->isHttpOnly()) {
$str .= '; httponly';
}

if (null !== $this->getSameSite()) {
$str .= '; samesite='.$this->getSameSite();
}

if ($this->isPartitioned()) {
$str .= '; partitioned';
}

return $str;
}

Expand Down Expand Up @@ -365,6 +386,14 @@ public function isRaw(): bool
return $this->raw;
}

/**
* Checks whether the cookie should be tied to the top-level site in cross-site context.
*/
public function isPartitioned(): bool
{
return $this->partitioned;
}

/**
* @return self::SAMESITE_*|null
*/
Expand Down
41 changes: 41 additions & 0 deletions src/Symfony/Component/HttpFoundation/Tests/CookieTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,19 @@ public function testNegativeExpirationIsNotPossible()
$this->assertSame(0, $cookie->getExpiresTime());
}

public function testMinimalParameters()
{
$constructedCookie = new Cookie('foo');

$createdCookie = Cookie::create('foo');

$cookie = new Cookie('foo', null, 0, '/', null, null, true, false, 'lax');

$this->assertEquals($constructedCookie, $cookie);

$this->assertEquals($createdCookie, $cookie);
}

public function testGetValue()
{
$value = 'MyValue';
Expand Down Expand Up @@ -187,6 +200,17 @@ public function testIsHttpOnly()
$this->assertTrue($cookie->isHttpOnly(), '->isHttpOnly() returns whether the cookie is only transmitted over HTTP');
}

public function testIsPartitioned()
{
$cookie = new Cookie('foo', 'bar', 0, '/', '.myfoodomain.com', true, true, false, 'Lax', true);

$this->assertTrue($cookie->isPartitioned());

$cookie = Cookie::create('foo')->withPartitioned(true);

$this->assertTrue($cookie->isPartitioned());
}

public function testCookieIsNotCleared()
CAB1 {
$cookie = Cookie::create('foo', 'bar', time() + 3600 * 24);
Expand Down Expand Up @@ -262,6 +286,20 @@ public function testToString()
->withSameSite(null);
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');

$expected = 'foo=deleted; expires='.gmdate('D, d M Y H:i:s T', $expire = time() - 31536001).'; Max-Age=0; path=/admin/; domain=.myfoodomain.com; secure; httponly; samesite=none; partitioned';
$cookie = new Cookie('foo', null, 1, '/admin/', '.myfoodomain.com', true, true, false, 'none', true);
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');

$cookie = Cookie::create('foo')
->withExpires(1)
->withPath('/admin/')
->withDomain('.myfoodomain.com')
->withSecure(true)
->withHttpOnly(true)
->withSameSite('none')
->withPartitioned(true);
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');

$expected = 'foo=bar; path=/; httponly; samesite=lax';
$cookie = Cookie::create('foo', 'bar');
$this->assertEquals($expected, (string) $cookie);
Expand Down Expand Up @@ -321,6 +359,9 @@ public function testFromString()

$cookie = Cookie::fromString('foo_cookie=foo==; expires=Tue, 22 Sep 2020 06:27:09 GMT; path=/');
$this->assertEquals(Cookie::create('foo_cookie', 'foo==', strtotime('Tue, 22 Sep 2020 06:27:09 GMT'), '/', null, false, false, true, null), $cookie);

$cookie = Cookie::fromString('foo_cookie=foo==; expires=Tue, 22 Sep 2020 06:27:09 GMT; path=/; secure; httponly; samesite=none; partitioned');
$this->assertEquals(new Cookie('foo_cookie', 'foo==', strtotime('Tue, 22 Sep 2020 06:27:09 GMT'), '/', null, true, true, true, 'none', true), $cookie);
}

public function testFromStringWithHttpOnly()
Expand Down
0