Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 44c8458

Browse files
committed
Add SameSite cookies to FrameWorkBundle
Uses `session.cookie_samesite` for PHP >= 7.3. For PHP < 7.3 it first does a session_start(), find the emitted header, changes it, and emits it again with the value for SameSite added.
1 parent 441322f commit 44c8458

File tree

9 files changed

+99
-26
lines changed

9 files changed

+99
-26
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
2020
use Symfony\Component\Config\Definition\ConfigurationInterface;
2121
use Symfony\Component\Form\Form;
22+
use Symfony\Component\HttpFoundation\Cookie;
2223
use Symfony\Component\Lock\Lock;
2324
use Symfony\Component\Lock\Store\SemaphoreStore;
2425
use Symfony\Component\Messenger\MessageBusInterface;
@@ -482,6 +483,7 @@ private function addSessionSection(ArrayNodeDefinition $rootNode)
482483
->scalarNode('cookie_domain')->end()
483484
->booleanNode('cookie_secure')->end()
484485
->booleanNode('cookie_httponly')->defaultTrue()->end()
486+
->enumNode('cookie_samesite')->values(array(null, Cookie::SAMESITE_LAX, Cookie::SAMESITE_STRICT))->defaultNull()->end()
485487
->booleanNode('use_cookies')->end()
486488
->scalarNode('gc_divisor')->end()
487489
->scalarNode('gc_probability')->defaultValue(1)->end()

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,7 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c
736736
// session storage
737737
$container->setAlias('session.storage', $config['storage_id'])->setPrivate(true);
738738
$options = array('cache_limiter' => '0');
739-
foreach (array('name', 'cookie_lifetime', 'cookie_path', 'cookie_domain', 'cookie_secure', 'cookie_httponly', 'use_cookies', 'gc_maxlifetime', 'gc_probability', 'gc_divisor') as $key) {
739+
foreach (array('name', 'cookie_lifetime', 'cookie_path', 'cookie_domain', 'cookie_secure', 'cookie_httponly', 'cookie_samesite', 'use_cookies', 'gc_maxlifetime', 'gc_probability', 'gc_divisor') as $key) {
740740
if (isset($config[$key])) {
741741
$options[$key] = $config[$key];
742742
}

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ protected static function getBundleDefaultConfig()
231231
'storage_id' => 'session.storage.native',
232232
'handler_id' => 'session.handler.native_file',
233233
'cookie_httponly' => true,
234+
'cookie_samesite' => null,
234235
'gc_probability' => 1,
235236
'save_path' => '%kernel.cache_dir%/sessions',
236237
'metadata_update_threshold' => '0',

src/Symfony/Component/HttpFoundation/HeaderUtils.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,42 @@ public static function unquote(string $s): string
143143
return preg_replace('/\\\\(.)|"/', '$1', $s);
144144
}
145145

146+
/**
147+
* Find the session header amongst the headers that are to be sent, remove it, and return
148+
* it so the caller can process it further.
149+
*/
150+
public static function popSessionCookie(string $sessionName, string $sessionId): ?string
151+
{
152+
$sessionCookie = null;
153+
$sessionCookiePrefix = sprintf(' %s=', urlencode($sessionName));
154+
$sessionCookieWithId = sprintf('%s%s;', $sessionCookiePrefix, urlencode($sessionId));
155+
$otherCookies = array();
156+
foreach (headers_list() as $h) {
157+
if (0 !== stripos($h, 'Set-Cookie:')) {
158+
continue;
159+
}
160+
if (11 === strpos($h, $sessionCookiePrefix, 11)) {
161+
$sessionCookie = $h;
162+
163+
if (11 !== strpos($h, $sessionCookieWithId, 11)) {
164+
$otherCookies[] = $h;
165+
}
166+
} else {
167+
$otherCookies[] = $h;
168+
}
169+
}
170+
if (null === $sessionCookie) {
171+
return null;
172+
}
173+
174+
header_remove('Set-Cookie');
175+
foreach ($otherCookies as $h) {
176+
header($h, false);
177+
}
178+
179+
return $sessionCookie;
180+
}
181+
146182
private static function groupParts(array $matches, string $separators): array
147183
{
148184
$separator = $separators[0];

src/Symfony/Component/HttpFoundation/Session/Storage/Handler/AbstractSessionHandler.php

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
1313

14+
use Symfony\Component\HttpFoundation\HeaderUtils;
15+
1416
/**
1517
* This abstract session handler provides a generic implementation
1618
* of the PHP 7.0 SessionUpdateTimestampHandlerInterface,
@@ -121,30 +123,8 @@ public function destroy($sessionId)
121123
if (!$this->sessionName) {
122124
throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', \get_class($this)));
123125
}
124-
$sessionCookie = sprintf(' %s=', urlencode($this->sessionName));
125-
$sessionCookieWithId = sprintf('%s%s;', $sessionCookie, urlencode($sessionId));
126-
$sessionCookieFound = false;
127-
$otherCookies = array();
128-
foreach (headers_list() as $h) {
129-
if (0 !== stripos($h, 'Set-Cookie:')) {
130-
continue;
131-
}
132-
if (11 === strpos($h, $sessionCookie, 11)) {
133-
$sessionCookieFound = true;
134-
135-
if (11 !== strpos($h, $sessionCookieWithId, 11)) {
136-
$otherCookies[] = $h;
137-
}
138-
} else {
139-
$otherCookies[] = $h;
140-
}
141-
}
142-
if ($sessionCookieFound) {
143-
header_remove('Set-Cookie');
144-
foreach ($otherCookies as $h) {
145-
header($h, false);
146-
}
147-
} else {
126+
$cookie = HeaderUtils::popSessionCookie($this->sessionName, $sessionId);
127+
if (null === $cookie) {
148128
setcookie($this->sessionName, '', 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure'), ini_get('session.cookie_httponly'));
149129
}
150130
}

src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\HttpFoundation\Session\Storage;
1313

14+
use Symfony\Component\HttpFoundation\Cookie;
15+
use Symfony\Component\HttpFoundation\HeaderUtils;
1416
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
1517
use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler;
1618
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
@@ -48,6 +50,11 @@ class NativeSessionStorage implements SessionStorageInterface
4850
*/
4951
protected $metadataBag;
5052

53+
/**
54+
* @var string|null
55+
*/
56+
private $emulateSameSite;
57+
5158
/**
5259
* Depending on how you want the storage driver to behave you probably
5360
* want to override this constructor entirely.
@@ -67,6 +74,7 @@ class NativeSessionStorage implements SessionStorageInterface
6774
* cookie_lifetime, "0"
6875
* cookie_path, "/"
6976
* cookie_secure, ""
77+
* cookie_samesite, null
7078
* gc_divisor, "100"
7179
* gc_maxlifetime, "1440"
7280
* gc_probability, "1"
@@ -143,6 +151,13 @@ public function start()
143151
throw new \RuntimeException('Failed to start the session');
144152
}
145153

154+
if (null !== $this->emulateSameSite) {
155+
$originalCookie = HeaderUtils::popSessionCookie(session_name(), session_id());
156+
if (null !== $originalCookie) {
157+
header(sprintf('%s; SameSite=%s', $originalCookie, $this->emulateSameSite));
158+
}
159+
}
160+
146161
$this->loadSession();
147162

148163
return true;
@@ -347,7 +362,7 @@ public function setOptions(array $options)
347362

348363
$validOptions = array_flip(array(
349364
'cache_expire', 'cache_limiter', 'cookie_domain', 'cookie_httponly',
350-
'cookie_lifetime', 'cookie_path', 'cookie_secure',
365+
'cookie_lifetime', 'cookie_path', 'cookie_secure', 'cookie_samesite',
351366
'gc_divisor', 'gc_maxlifetime', 'gc_probability',
352367
'lazy_write', 'name', 'referer_check',
353368
'serialize_handler', 'use_strict_mode', 'use_cookies',
@@ -359,6 +374,12 @@ public function setOptions(array $options)
359374

360375
foreach ($options as $key => $value) {
361376
if (isset($validOptions[$key])) {
377+
if ('cookie_samesite' === $key && \PHP_VERSION_ID < 70300) {
378+
// PHP <= 7.3 does not support same_site cookies. We will emulate it in
379+
// the start() method instead.
380+
$this->emulateSameSite = $value;
381+
continue;
382+
}
362383
ini_set('url_rewriter.tags' !== $key ? 'session.'.$key : $key, $value);
363384
}
364385
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
open
2+
validateId
3+
read
4+
doRead:
5+
read
6+
7+
write
8+
doWrite: foo|s:3:"bar";
9+
close
10+
Array
11+
(
12+
[0] => Content-Type: text/plain; charset=utf-8
13+
[1] => Cache-Control: max-age=0, private, must-revalidate
14+
[2] => Set-Cookie: sid=random_session_id; path=/; secure; HttpOnly; SameSite=lax
15+
)
16+
shutdown
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
require __DIR__.'/common.inc';
4+
5+
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
6+
7+
$storage = new NativeSessionStorage(array('cookie_samesite' => 'lax'));
8+
$storage->setSaveHandler(new TestSessionHandler());
9+
$storage->start();
10+
11+
$_SESSION = array('foo' => 'bar');
12+
13+
ob_start(function ($buffer) { return str_replace(session_id(), 'random_session_id', $buffer); });

src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ public function testCookieOptions()
171171
'cookie_httponly' => false,
172172
);
173173

174+
if (\PHP_VERSION_ID >= 70300) {
175+
$options['cookie_samesite'] = 'lax';
176+
}
177+
174178
$this->getStorage($options);
175179
$temp = session_get_cookie_params();
176180
$gco = array();

0 commit comments

Comments
 (0)