Skip to content

Commit 966f278

Browse files
author
hwyu@adobe.com
committed
MC-39528: Introduce explicit support for samesite attribute in 2.3.x
- Ported changes from MC-37531
1 parent 263f508 commit 966f278

File tree

10 files changed

+433
-77
lines changed

10 files changed

+433
-77
lines changed

dev/tests/integration/testsuite/Magento/Framework/Stdlib/Cookie/CookieScopeTest.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,21 @@ public function testGetSensitiveCookieMetadataEmpty()
4343
[
4444
SensitiveCookieMetadata::KEY_HTTP_ONLY => true,
4545
SensitiveCookieMetadata::KEY_SECURE => true,
46+
SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax'
4647
],
4748
$cookieScope->getSensitiveCookieMetadata()->__toArray()
4849
);
4950

5051
$this->request->setServer(new Parameters($serverVal));
5152
}
5253

53-
public function testGetPublicCookieMetadataEmpty()
54+
public function testGetPublicCookieDefaultMetadata()
5455
{
5556
$cookieScope = $this->createCookieScope();
56-
57-
$this->assertEmpty($cookieScope->getPublicCookieMetadata()->__toArray());
57+
$expected = [
58+
PublicCookieMetadata::KEY_SAME_SITE => 'Lax'
59+
];
60+
$this->assertEquals($expected, $cookieScope->getPublicCookieMetadata()->__toArray());
5861
}
5962

6063
public function testGetSensitiveCookieMetadataDefaults()
@@ -77,6 +80,7 @@ public function testGetSensitiveCookieMetadataDefaults()
7780
SensitiveCookieMetadata::KEY_DOMAIN => 'default domain',
7881
SensitiveCookieMetadata::KEY_HTTP_ONLY => true,
7982
SensitiveCookieMetadata::KEY_SECURE => false,
83+
SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax',
8084
],
8185
$cookieScope->getSensitiveCookieMetadata()->__toArray()
8286
);
@@ -90,6 +94,7 @@ public function testGetPublicCookieMetadataDefaults()
9094
PublicCookieMetadata::KEY_DURATION => 'default duration',
9195
PublicCookieMetadata::KEY_HTTP_ONLY => 'default http',
9296
PublicCookieMetadata::KEY_SECURE => 'default secure',
97+
PublicCookieMetadata::KEY_SAME_SITE => 'Lax',
9398
];
9499
$public = $this->createPublicMetadata($defaultValues);
95100
$cookieScope = $this->createCookieScope(
@@ -139,6 +144,7 @@ public function testGetSensitiveCookieMetadataOverrides()
139144
SensitiveCookieMetadata::KEY_DOMAIN => 'override domain',
140145
SensitiveCookieMetadata::KEY_HTTP_ONLY => true,
141146
SensitiveCookieMetadata::KEY_SECURE => false,
147+
SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax',
142148
],
143149
$cookieScope->getSensitiveCookieMetadata($override)->__toArray()
144150
);
@@ -159,6 +165,7 @@ public function testGetPublicCookieMetadataOverrides()
159165
PublicCookieMetadata::KEY_DURATION => 'override duration',
160166
PublicCookieMetadata::KEY_HTTP_ONLY => 'override http',
161167
PublicCookieMetadata::KEY_SECURE => 'override secure',
168+
PublicCookieMetadata::KEY_SAME_SITE => 'Lax',
162169
];
163170
$public = $this->createPublicMetadata($defaultValues);
164171
$cookieScope = $this->createCookieScope(

lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
* Copyright © Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
6+
declare(strict_types=1);
7+
68
namespace Magento\Framework\Stdlib\Cookie;
79

810
/**
9-
* Class CookieMetadata
11+
* Cookie Attributes
1012
* @api
1113
* @since 100.0.2
1214
*/
@@ -15,11 +17,17 @@ class CookieMetadata
1517
/**#@+
1618
* Constant for metadata value key.
1719
*/
18-
const KEY_DOMAIN = 'domain';
19-
const KEY_PATH = 'path';
20-
const KEY_SECURE = 'secure';
21-
const KEY_HTTP_ONLY = 'http_only';
22-
const KEY_DURATION = 'duration';
20+
public const KEY_DOMAIN = 'domain';
21+
public const KEY_PATH = 'path';
22+
public const KEY_SECURE = 'secure';
23+
public const KEY_HTTP_ONLY = 'http_only';
24+
public const KEY_DURATION = 'duration';
25+
public const KEY_SAME_SITE = 'samesite';
26+
private const SAME_SITE_ALLOWED_VALUES = [
27+
'strict' => 'Strict',
28+
'lax' => 'Lax',
29+
'none' => 'None',
30+
];
2331
/**#@-*/
2432

2533
/**#@-*/
@@ -34,6 +42,9 @@ public function __construct($metadata = [])
3442
$metadata = [];
3543
}
3644
$this->metadata = $metadata;
45+
if (isset($metadata[self::KEY_SAME_SITE])) {
46+
$this->setSameSite($metadata[self::KEY_SAME_SITE]);
47+
}
3748
}
3849

3950
/**
@@ -43,7 +54,7 @@ public function __construct($metadata = [])
4354
*
4455
* @return array
4556
*/
46-
public function __toArray()
57+
public function __toArray() //phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames
4758
{
4859
return $this->metadata;
4960
}
@@ -136,4 +147,36 @@ public function getSecure()
136147
{
137148
return $this->get(self::KEY_SECURE);
138149
}
150+
151+
/**
152+
* Setter for Cookie SameSite attribute
153+
*
154+
* @param string $sameSite
155+
* @return $this
156+
*/
157+
public function setSameSite(string $sameSite): CookieMetadata
158+
{
159+
if (!array_key_exists(strtolower($sameSite), self::SAME_SITE_ALLOWED_VALUES)) {
160+
throw new \InvalidArgumentException(
161+
'Invalid argument provided for SameSite directive expected one of: Strict, Lax or None'
162+
);
163+
}
164+
if (!$this->getSecure() && strtolower($sameSite) === 'none') {
165+
throw new \InvalidArgumentException(
166+
'Cookie must be secure in order to use the SameSite None directive.'
167+
);
168+
}
169+
$sameSite = self::SAME_SITE_ALLOWED_VALUES[strtolower($sameSite)];
170+
return $this->set(self::KEY_SAME_SITE, $sameSite);
171+
}
172+
173+
/**
174+
* Get Same Site Flag
175+
*
176+
* @return string
177+
*/
178+
public function getSameSite(): string
179+
{
180+
return $this->get(self::KEY_SAME_SITE);
181+
}
139182
}

lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php

Lines changed: 121 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Copyright © Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
6+
declare(strict_types=1);
67

78
namespace Magento\Framework\Stdlib\Cookie;
89

@@ -21,6 +22,7 @@
2122
* stores the cookie.
2223
*
2324
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
25+
* @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
2426
*/
2527
class PhpCookieManager implements CookieManagerInterface
2628
{
@@ -63,6 +65,18 @@ class PhpCookieManager implements CookieManagerInterface
6365
*/
6466
private $httpHeader;
6567

68+
/**#@+
69+
* Constant for SameSite Supported Php Version
70+
*/
71+
private const SAMESITE_SUPPORTED_PHP_VERSION = '7.3';
72+
/**#@-*/
73+
74+
/**#@+
75+
* Constant for Set-Cookie Header
76+
*/
77+
private const COOKIE_HEADER = 'Set-Cookie:';
78+
/**#@-*/
79+
6680
/**
6781
* @param CookieScopeInterface $scope
6882
* @param CookieReaderInterface $reader
@@ -98,7 +112,7 @@ public function __construct(
98112
public function setSensitiveCookie($name, $value, SensitiveCookieMetadata $metadata = null)
99113
{
100114
$metadataArray = $this->scope->getSensitiveCookieMetadata($metadata)->__toArray();
101-
$this->setCookie($name, $value, $metadataArray);
115+
$this->setCookie((string)$name, (string)$value, $metadataArray);
102116
}
103117

104118
/**
@@ -118,7 +132,7 @@ public function setSensitiveCookie($name, $value, SensitiveCookieMetadata $metad
118132
public function setPublicCookie($name, $value, PublicCookieMetadata $metadata = null)
119133
{
120134
$metadataArray = $this->scope->getPublicCookieMetadata($metadata)->__toArray();
121-
$this->setCookie($name, $value, $metadataArray);
135+
$this->setCookie((string)$name, (string)$value, $metadataArray);
122136
}
123137

124138
/**
@@ -138,32 +152,41 @@ protected function setCookie($name, $value, array $metadataArray)
138152

139153
$this->checkAbilityToSendCookie($name, $value);
140154

141-
$phpSetcookieSuccess = setcookie(
142-
$name,
143-
$value,
144-
$expire,
145-
$this->extractValue(CookieMetadata::KEY_PATH, $metadataArray, ''),
146-
$this->extractValue(CookieMetadata::KEY_DOMAIN, $metadataArray, ''),
147-
$this->extractValue(CookieMetadata::KEY_SECURE, $metadataArray, false),
148-
$this->extractValue(CookieMetadata::KEY_HTTP_ONLY, $metadataArray, false)
149-
);
150-
151-
if (!$phpSetcookieSuccess) {
152-
$params['name'] = $name;
153-
if ($value == '') {
154-
throw new FailureToSendException(
155-
new Phrase('The cookie with "%name" cookieName couldn\'t be deleted.', $params)
156-
);
157-
} else {
158-
throw new FailureToSendException(
159-
new Phrase('The cookie with "%name" cookieName couldn\'t be sent. Please try again later.', $params)
160-
);
155+
if (version_compare(phpversion(), self::SAMESITE_SUPPORTED_PHP_VERSION, '>=')) {
156+
157+
$phpSetcookieSuccess = setcookie(
158+
$name,
159+
$value,
160+
[
161+
'expires' => $expire,
162+
'path' => $this->extractValue(CookieMetadata::KEY_PATH, $metadataArray, ''),
163+
'domain' => $this->extractValue(CookieMetadata::KEY_DOMAIN, $metadataArray, ''),
164+
'secure' => $this->extractValue(CookieMetadata::KEY_SECURE, $metadataArray, false),
165+
'httponly' => $this->extractValue(CookieMetadata::KEY_HTTP_ONLY, $metadataArray, false),
166+
'samesite' => $this->extractValue(CookieMetadata::KEY_SAME_SITE, $metadataArray, 'Lax')
167+
]
168+
);
169+
if (!$phpSetcookieSuccess) {
170+
$params['name'] = $name;
171+
if ($value == '') {
172+
throw new FailureToSendException(
173+
new Phrase('The cookie with "%name" cookieName couldn\'t be deleted.', $params)
174+
);
175+
} else {
176+
$exceptionMessage = 'The cookie with "%name" cookieName couldn\'t be sent. Please try again later.';
177+
throw new FailureToSendException(
178+
new Phrase($exceptionMessage, $params)
179+
);
180+
}
161181
}
182+
} else {
183+
$this->setCookieSameSite($name, $value, $metadataArray);
162184
}
163185
}
164186

165187
/**
166188
* Retrieve the size of a cookie.
189+
*
167190
* The size of a cookie is determined by the length of 'name=value' portion of the cookie.
168191
*
169192
* @param string $name
@@ -177,8 +200,7 @@ private function sizeOfCookie($name, $value)
177200
}
178201

179202
/**
180-
* Determines whether or not it is possible to send the cookie, based on the number of cookies that already
181-
* exist and the size of the cookie.
203+
* Determines ability to send cookies, based on the number of existing cookies and cookie size
182204
*
183205
* @param string $name
184206
* @param string|null $value
@@ -249,6 +271,7 @@ private function computeExpirationTime(array $metadataArray)
249271

250272
/**
251273
* Determines the value to be used as a $parameter.
274+
*
252275
* If $metadataArray[$parameter] is not set, returns the $defaultValue.
253276
*
254277
* @param string $parameter
@@ -303,4 +326,78 @@ public function deleteCookie($name, CookieMetadata $metadata = null)
303326
// Remove the cookie
304327
unset($_COOKIE[$name]);
305328
}
329+
330+
/**
331+
* Polyfill for Set-Cookie with support for SameSite attribute
332+
*
333+
* Supports Php version 7.2 and lower
334+
*
335+
* @param string $name
336+
* @param string $value
337+
* @param array $metadataArray
338+
* @throws FailureToSendException
339+
* @return void
340+
*/
341+
private function setCookieSameSite(string $name, string $value, array $metadataArray): void
342+
{
343+
344+
$expires = $this->computeExpirationTime($metadataArray);
345+
$path = $this->extractValue(CookieMetadata::KEY_PATH, $metadataArray, '');
346+
$domain = $this->extractValue(CookieMetadata::KEY_DOMAIN, $metadataArray, '');
347+
$secure = $this->extractValue(CookieMetadata::KEY_SECURE, $metadataArray, false);
348+
$httpOnly = $this->extractValue(CookieMetadata::KEY_HTTP_ONLY, $metadataArray, false);
349+
$sameSite = $this->extractValue(CookieMetadata::KEY_SAME_SITE, $metadataArray, 'Lax');
350+
$params = [];
351+
$setCookieSuccess = false;
352+
353+
if ('' === $value) {
354+
$params[] = $name . '=' . 'deleted';
355+
356+
} else {
357+
$params[] = $name . '=' . rawurlencode($value);
358+
}
359+
360+
if (0 !== $expires) {
361+
$formattedExpirationTime = gmdate('D, d-M-Y H:i:s T', $expires);
362+
$params[] = sprintf('expires=%s', $formattedExpirationTime);
363+
}
364+
365+
if ($path) {
366+
$params[] = sprintf('path=%s', $path);
367+
}
368+
369+
if ($domain) {
370+
$params[] = sprintf('domain=%s', $domain);
371+
}
372+
373+
if ($httpOnly) {
374+
$params[] = 'HttpOnly';
375+
}
376+
377+
if ($secure) {
378+
$params[] = 'secure';
379+
}
380+
381+
$params[] = sprintf('SameSite=%s', $sameSite);
382+
$header = sprintf(self::COOKIE_HEADER . "%s", implode('; ', $params));
383+
header($header, false);
384+
385+
$setCookieSuccess = array_filter(headers_list(), function ($value) use ($header) {
386+
return strpos($value, $header) !== false;
387+
});
388+
389+
if (!$setCookieSuccess) {
390+
$args['name'] = $name;
391+
if ($value == '') {
392+
throw new FailureToSendException(
393+
new Phrase('The cookie with "%name" cookieName couldn\'t be deleted.', $args)
394+
);
395+
} else {
396+
$exceptionMessage = 'The cookie with "%name" cookieName couldn\'t be sent. Please try again later.';
397+
throw new FailureToSendException(
398+
new Phrase($exceptionMessage, $args)
399+
);
400+
}
401+
}
402+
}
306403
}

lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,24 @@
77
namespace Magento\Framework\Stdlib\Cookie;
88

99
/**
10-
* Class PublicCookieMetadata
10+
* Public Cookie Attributes
1111
*
1212
* @api
1313
* @since 100.0.2
1414
*/
1515
class PublicCookieMetadata extends CookieMetadata
1616
{
17+
/**
18+
* @param array $metadata
19+
*/
20+
public function __construct($metadata = [])
21+
{
22+
if (!isset($metadata[self::KEY_SAME_SITE])) {
23+
$metadata[self::KEY_SAME_SITE] = 'Lax';
24+
}
25+
parent::__construct($metadata);
26+
}
27+
1728
/**
1829
* Set the number of seconds until the cookie expires
1930
*
@@ -68,6 +79,11 @@ public function setHttpOnly($httpOnly)
6879
*/
6980
public function setSecure($secure)
7081
{
82+
if (!$secure && $this->get(self::KEY_SAME_SITE) === 'None') {
83+
throw new \InvalidArgumentException(
84+
'Cookie must be secure in order to use the SameSite None directive.'
85+
);
86+
}
7187
return $this->set(self::KEY_SECURE, $secure);
7288
}
7389
}

0 commit comments

Comments
 (0)