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

Skip to content

Commit 4091feb

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 bedd7aa commit 4091feb

File tree

12 files changed

+129
-27
lines changed

12 files changed

+129
-27
lines changed

UPGRADE-4.2.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ Form
4242

4343
* Deprecated calling `FormRenderer::searchAndRenderBlock` for fields which were already rendered.
4444
Instead of expecting such calls to return empty strings, check if the field has already been rendered.
45-
45+
4646
Before:
4747
```twig
4848
{% for field in fieldsWithPotentialDuplicates %}
4949
{{ form_widget(field) }}
5050
{% endfor %}
5151
```
52-
52+
5353
After:
5454
```twig
5555
{% for field in fieldsWithPotentialDuplicates if not field.rendered %}
@@ -83,6 +83,7 @@ FrameworkBundle
8383
is UTF-8 (see kernel's `getCharset()` method), it is recommended to set it to `true`:
8484
this will generate 404s for non-UTF-8 URLs, which are incompatible with you app anyway,
8585
and will allow dumping optimized routers and using Unicode classes in requirements.
86+
* Added support for the SameSite attribute for session cookies. It is highly recommended to set this setting (`framework.session.cookie_samesite`) to `lax` for increased security against CSRF attacks.
8687

8788
Messenger
8889
---------

UPGRADE-5.0.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ FrameworkBundle
9292
* Warming up a router in `RouterCacheWarmer` that does not implement the `WarmableInterface` is not supported anymore.
9393
* The `RequestDataCollector` class has been removed. Use the `Symfony\Component\HttpKernel\DataCollector\RequestDataCollector` class instead.
9494
* Removed `Symfony\Bundle\FrameworkBundle\Controller\Controller`. Use `Symfony\Bundle\FrameworkBundle\Controller\AbstractController` instead.
95+
* Added support for the SameSite attribute for session cookies. It is highly recommended to set this setting (`framework.session.cookie_samesite`) to `lax` for increased security against CSRF attacks.
9596

9697
HttpFoundation
9798
--------------

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ CHANGELOG
2929
* Deprecated `Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser`
3030
* The `container.service_locator` tag of `ServiceLocator`s is now autoconfigured.
3131
* Add the ability to search a route in `debug:router`.
32+
* Add the ability to use SameSite cookies for sessions.
3233

3334
4.0.0
3435
-----

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;
@@ -484,6 +485,7 @@ private function addSessionSection(ArrayNodeDefinition $rootNode)
484485
->scalarNode('cookie_domain')->end()
485486
->enumNode('cookie_secure')->values(array(true, false, 'auto'))->end()
486487
->booleanNode('cookie_httponly')->defaultTrue()->end()
488+
->enumNode('cookie_samesite')->values(array(null, Cookie::SAMESITE_LAX, Cookie::SAMESITE_STRICT))->defaultNull()->end()
487489
->booleanNode('use_cookies')->end()
488490
->scalarNode('gc_divisor')->end()
489491
->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
@@ -757,7 +757,7 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c
757757
// session storage
758758
$container->setAlias('session.storage', $config['storage_id'])->setPrivate(true);
759759
$options = array('cache_limiter' => '0');
760-
foreach (array('name', 'cookie_lifetime', 'cookie_path', 'cookie_domain', 'cookie_secure', 'cookie_httponly', 'use_cookies', 'gc_maxlifetime', 'gc_probability', 'gc_divisor') as $key) {
760+
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) {
761761
if (isset($config[$key])) {
762762
$options[$key] = $config[$key];
763763
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ protected static function getBundleDefaultConfig()
232232
'storage_id' => 'session.storage.native',
233233
'handler_id' => 'session.handler.native_file',
234234
'cookie_httponly' => true,
235+
'cookie_samesite' => null,
235236
'gc_probability' => 1,
236237
'save_path' => '%kernel.cache_dir%/sessions',
237238
'metadata_update_threshold' => '0',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Session;
13+
14+
/**
15+
* Session utility functions.
16+
*
17+
* @author Nicolas Grekas <[email protected]>
18+
* @author Rémon van de Kamp <[email protected]>
19+
*
20+
* @internal
21+
*/
22+
final class SessionUtils
23+
{
24+
/**
25+
* Find the session header amongst the headers that are to be sent, remove it, and return
26+
* it so the caller can process it further.
27+
*/
28+
public static function popSessionCookie(string $sessionName, string $sessionId): ?string
29+
{
30+
$sessionCookie = null;
31+
$sessionCookiePrefix = sprintf(' %s=', urlencode($sessionName));
32+
$sessionCookieWithId = sprintf('%s%s;', $sessionCookiePrefix, urlencode($sessionId));
33+
$otherCookies = array();
34+
foreach (headers_list() as $h) {
35+
if (0 !== stripos($h, 'Set-Cookie:')) {
36+
continue;
37+
}
38+
if (11 === strpos($h, $sessionCookiePrefix, 11)) {
39+
$sessionCookie = $h;
40+
41+
if (11 !== strpos($h, $sessionCookieWithId, 11)) {
42+
$otherCookies[] = $h;
43+
}
44+
} else {
45+
$otherCookies[] = $h;
46+
}
47+
}
48+
if (null === $sessionCookie) {
49+
return null;
50+
}
51+
52+
header_remove('Set-Cookie');
53+
foreach ($otherCookies as $h) {
54+
header($h, false);
55+
}
56+
57+
return $sessionCookie;
58+
}
59+
}

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

Lines changed: 7 additions & 23 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\Session\SessionUtils;
15+
1416
/**
1517
* This abstract session handler provides a generic implementation
1618
* of the PHP 7.0 SessionUpdateTimestampHandlerInterface,
@@ -121,31 +123,13 @@ 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-
}
126+
$cookie = SessionUtils::popSessionCookie($this->sessionName, $sessionId);
127+
if (null === $cookie) {
128+
if (\PHP_VERSION_ID < 70300) {
129+
setcookie($this->sessionName, '', 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure'), ini_get('session.cookie_httponly'));
138130
} else {
139-
$otherCookies[] = $h;
140-
}
141-
}
142-
if ($sessionCookieFound) {
143-
header_remove('Set-Cookie');
144-
foreach ($otherCookies as $h) {
145-
header($h, false);
131+
setcookie($this->sessionName, '', 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure'), ini_get('session.cookie_httponly'), ini_get('session.cookie_samesite'));
146132
}
147-
} else {
148-
setcookie($this->sessionName, '', 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure'), ini_get('session.cookie_httponly'));
149133
}
150134
}
151135

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\HttpFoundation\Session\Storage;
1313

1414
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
15+
use Symfony\Component\HttpFoundation\Session\SessionUtils;
1516
use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler;
1617
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
1718
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
@@ -48,6 +49,11 @@ class NativeSessionStorage implements SessionStorageInterface
4849
*/
4950
protected $metadataBag;
5051

52+
/**
53+
* @var string|null
54+
*/
55+
private $emulateSameSite;
56+
5157
/**
5258
* Depending on how you want the storage driver to behave you probably
5359
* want to override this constructor entirely.
@@ -67,6 +73,7 @@ class NativeSessionStorage implements SessionStorageInterface
6773
* cookie_lifetime, "0"
6874
* cookie_path, "/"
6975
* cookie_secure, ""
76+
* cookie_samesite, null
7077
* gc_divisor, "100"
7178
* gc_maxlifetime, "1440"
7279
* gc_probability, "1"
@@ -143,6 +150,13 @@ public function start()
143150
throw new \RuntimeException('Failed to start the session');
144151
}
145152

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

148162
return true;
@@ -347,7 +361,7 @@ public function setOptions(array $options)
347361

348362
$validOptions = array_flip(array(
349363
'cache_expire', 'cache_limiter', 'cookie_domain', 'cookie_httponly',
350-
'cookie_lifetime', 'cookie_path', 'cookie_secure',
364+
'cookie_lifetime', 'cookie_path', 'cookie_secure', 'cookie_samesite',
351365
'gc_divisor', 'gc_maxlifetime', 'gc_probability',
352366
'lazy_write', 'name', 'referer_check',
353367
'serialize_handler', 'use_strict_mode', 'use_cookies',
@@ -359,6 +373,12 @@ public function setOptions(array $options)
359373

360374
foreach ($options as $key => $value) {
361375
if (isset($validOptions[$key])) {
376+
if ('cookie_samesite' === $key && \PHP_VERSION_ID < 70300) {
377+
// PHP < 7.3 does not support same_site cookies. We will emulate it in
378+
// the start() method instead.
379+
$this->emulateSameSite = $value;
380+
continue;
381+
}
362382
ini_set('url_rewriter.tags' !== $key ? 'session.'.$key : $key, $value);
363383
}
364384
}
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)