8000 Add SameSite cookies to FrameWorkBundle · symfony/symfony@44c8458 · GitHub
[go: up one dir, main page]

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
  • Tests/Session/Storage
  • 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
    F438
    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)
    0