8000 feature #52002 [HttpFoundation] Cookies Having Independent Partitione… · symfony/symfony@f13a4b1 · GitHub
[go: up one dir, main page]

Skip to content

Commit f13a4b1

Browse files
committed
feature #52002 [HttpFoundation] Cookies Having Independent Partitioned State (CHIPS) (fabricecw)
This PR was merged into the 6.4 branch. Discussion ---------- [HttpFoundation] Cookies Having Independent Partitioned State (CHIPS) | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT Due to [Chrome's roadmap](https://developer.chrome.com/docs/privacy-sandbox/third-party-cookie-phase-out/) (and all other major browsers) to phase out third-party cookies starting from midway through 2024, "partitioned" cookies were introduced. If a cookie is flagged as `partitioned`, its cross-site boundry is tied to the top-level site. Considerations: According to current security design, browser will only accept partitioned cookies with the `secure` flag and `SameSite` attribute `none` (otherwise it isn't a third-party cookie...). I classified this as an implementation topic and therefore omitted this validation in the Cookie class itself. Further information: - [Chrome for Developers](https://developer.chrome.com/docs/privacy-sandbox/chips/) - [Mozilla Dev](https://developer.mozilla.org/en-US/docs/Web/Privacy/Partitioned_cookies) - [CHIPS](https://github.com/privacycg/CHIPS) Commits ------- 26df07b [HttpFoundation] Cookies Having Independent Partitioned State (CHIPS)
2 parents f4e6c39 + 26df07b commit f13a4b1

File tree

3 files changed

+77
-6
lines changed

3 files changed

+77
-6
lines changed

src/Symfony/Component/HttpFoundation/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Make `HeaderBag::getDate()`, `Response::getDate()`, `getExpires()` and `getLastModified()` return a `DateTimeImmutable`
88
* Support root-level `Generator` in `StreamedJsonResponse`
99
* Add `UriSigner` from the HttpKernel component
10+
* Add `partitioned` flag to `Cookie` (CHIPS Cookie)
1011

1112
6.3
1213
---

src/Symfony/Component/HttpFoundation/Cookie.php

+35-6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class Cookie
3232

3333
private bool $raw;
3434
private ?string $sameSite = null;
35+
private bool $partitioned = false;
3536
private bool $secureDefault = false;
3637

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

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

69-
return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']);
71+
return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite'], $data['partitioned']);
7072
}
7173

7274
/**
7375
* @see self::__construct
7476
*
7577
* @param self::SAMESITE_*|''|null $sameSite
78+
* @param bool $partitioned
7679
*/
77-
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
80+
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
7881
{
79-
return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite);
82+
$partitioned = 9 < \func_num_args() ? func_get_arg(9) : false;
83+
84+
return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite, $partitioned);
8085
}
8186

8287
/**
@@ -92,7 +97,7 @@ public static function create(string $name, string $value = null, int|string|\Da
9297
*
9398
* @throws \InvalidArgumentException
9499
*/
95-
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)
100+
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)
96101
{
97102
// from PHP source code
98103
if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) {
@@ -112,6 +117,7 @@ public function __construct(string $name, string $value = null, int|string|\Date
112117
$this->httpOnly = $httpOnly;
113118
$this->raw = $raw;
114119
$this->sameSite = $this->withSameSite($sameSite)->sameSite;
120+
$this->partitioned = $partitioned;
115121
}
116122

117123
/**
@@ -237,6 +243,17 @@ public function withSameSite(?string $sameSite): static
237243
return $cookie;
238244
}
239245

246+
/**
247+
* Creates a cookie copy that is tied to the top-level site in cross-site context.
248+
*/
249+
public function withPartitioned(bool $partitioned = true): static
250+
{
251+
$cookie = clone $this;
252+
$cookie->partitioned = $partitioned;
253+
254+
return $cookie;
255+
}
256+
240257
/**
241258
* Returns the cookie as a string.
242259
*/
@@ -268,18 +285,22 @@ public function __toString(): string
268285
$str .= '; domain='.$this->getDomain();
269286
}
270287

271-
if (true === $this->isSecure()) {
288+
if ($this->isSecure()) {
272289
$str .= '; secure';
273290
}
274291

275-
if (true === $this->isHttpOnly()) {
292+
if ($this->isHttpOnly()) {
276293
$str .= '; httponly';
277294
}
278295

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

300+
if ($this->isPartitioned()) {
301+
$str .= '; partitioned';
302+
}
303+
283304
return $str;
284305
}
285306

@@ -365,6 +386,14 @@ public function isRaw(): bool
365386
return $this->raw;
366387
}
367388

389+
/**
390+
* Checks whether the cookie should be tied to the top-level site in cross-site context.
391+
*/
392+
public function isPartitioned(): bool
393+
{
394+
return $this->partitioned;
395+
}
396+
368397
/**
369398
* @return self::SAMESITE_*|null
370399
*/

src/Symfony/Component/HttpFoundation/Tests/CookieTest.php

+41
< 10000 /tr>
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,19 @@ public function testNegativeExpirationIsNotPossible()
8787
$this->assertSame(0, $cookie->getExpiresTime());
8888
}
8989

90+
public function testMinimalParameters()
91+
{
92+
$constructedCookie = new Cookie('foo');
93+
94+
$createdCookie = Cookie::create('foo');
95+
96+
$cookie = new Cookie('foo', null, 0, '/', null, null, true, false, 'lax');
97+
98+
$this->assertEquals($constructedCookie, $cookie);
99+
100+
$this->assertEquals($createdCookie, $cookie);
101+
}
102+
90103
public function testGetValue()
91104
{
92105
$value = 'MyValue';
@@ -187,6 +200,17 @@ public function testIsHttpOnly()
187200
$this->assertTrue($cookie->isHttpOnly(), '->isHttpOnly() returns whether the cookie is only transmitted over HTTP');
188201
}
189202

203+
public function testIsPartitioned()
204+
{
205+
$cookie = new Cookie('foo', 'bar', 0, '/', '.myfoodomain.com', true, true, false, 'Lax', true);
206+
207+
$this->assertTrue($cookie->isPartitioned());
208+
209+
$cookie = Cookie::create('foo')->withPartitioned(true);
210+
211+
$this->assertTrue($cookie->isPartitioned());
212+
}
213+
190214
public function testCookieIsNotCleared()
191215
{
192216
$cookie = Cookie::create('foo', 'bar', time() + 3600 * 24);
@@ -262,6 +286,20 @@ public function testToString()
262286
->withSameSite(null);
263287
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');
264288

289+
$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';
290+
$cookie = new Cookie('foo', null, 1, '/admin/', '.myfoodomain.com', true, true, false, 'none', true);
291+
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');
292+
293+
$cookie = Cookie::create('foo')
294+
->withExpires(1)
295+
->withPath('/admin/')
296+
->withDomain('.myfoodomain.com')
297+
->withSecure(true)
298+
->withHttpOnly(true)
299+
->withSameSite('none')
300+
->withPartitioned(true);
301+
$this->assertEquals($expected, (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');
302+
265303
$expected = 'foo=bar; path=/; httponly; samesite=lax';
266304
$cookie = Cookie::create('foo', 'bar');
267305
$this->assertEquals($expected, (string) $cookie);
@@ -321,6 +359,9 @@ public function testFromString()
321359

322360
$cookie = Cookie::fromString('foo_cookie=foo==; expires=Tue, 22 Sep 2020 06:27:09 GMT; path=/');
323361
$this->assertEquals(Cookie::create('foo_cookie', 'foo==', strtotime('Tue, 22 Sep 2020 06:27:09 GMT'), '/', null, false, false, true, null), $cookie);
362+
363+
$cookie = Cookie::fromString('foo_cookie=foo==; expires=Tue, 22 Sep 2020 06:27:09 GMT; path=/; secure; httponly; samesite=none; partitioned');
364+
$this->assertEquals(new Cookie('foo_cookie', 'foo==', strtotime('Tue, 22 Sep 2020 06:27:09 GMT'), '/', null, true, true, true, 'none', true), $cookie);
324365
}
325366

326367
public function testFromStringWithHttpOnly()

0 commit comments

Comments
 (0)
0