Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SameSite cookie support #4519

Merged
merged 8 commits into from May 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions app/config/eccube/packages/framework.yaml
Expand Up @@ -8,10 +8,9 @@ framework:
trusted_hosts: ~
# https://symfony.com/doc/current/reference/configuration/framework.html#handler-id
session:
handler_id: session.handler.native_file
handler_id: 'Eccube\Session\Storage\Handler\SameSiteNoneCompatSessionHandler'
save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
name: '%env(ECCUBE_COOKIE_NAME)%'
cookie_path: '%env(ECCUBE_COOKIE_PATH)%'
cookie_lifetime: '%env(ECCUBE_COOKIE_LIFETIME)%'
gc_maxlifetime: '%env(ECCUBE_GC_MAXLIFETIME)%'
cookie_httponly: true
Expand Down
7 changes: 7 additions & 0 deletions app/config/eccube/services.yaml
Expand Up @@ -171,3 +171,10 @@ services:
# Symfony\Bridge\Twig\Extension\RoutingExtensionの後に登録するため,
# autoconfigureはfalseにし, CompilerPassで追加する.
autoconfigure: false

native_file_session_handler:
class: Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler

Eccube\Session\Storage\Handler\SameSiteNoneCompatSessionHandler:
arguments:
- '@native_file_session_handler'
1 change: 1 addition & 0 deletions composer.json
Expand Up @@ -50,6 +50,7 @@
"sensio/generator-bundle": "^3.1",
"setasign/fpdi": "^2.2",
"setasign/fpdi-tcpdf": "^2.2",
"skorp/detect-incompatible-samesite-useragents": "^1.0",
"suncat/mobile-detect-bundle": "^1.1",
"swiftmailer/swiftmailer": "^6.1",
"symfony/asset": "^3.4",
Expand Down
56 changes: 53 additions & 3 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -0,0 +1,213 @@
<?php

/*
* This file is part of EC-CUBE
*
* Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
*
* http://www.ec-cube.co.jp/
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Eccube\Session\Storage\Handler;

use Skorp\Dissua\SameSite;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler;

class SameSiteNoneCompatSessionHandler extends StrictSessionHandler
{
/** @var \SessionHandlerInterface */
private $handler;
/** @var bool */
private $doDestroy;
/** @var string */
private $sessionName;
/** @var string|null */
private $prefetchId;
/** @var string|null */
private $prefetchData;
/** @var string */
private $newSessionId;
/** @var string|null */
private $igbinaryEmptyData;

/**
* {@inheritdoc}
*/
public function __construct(\SessionHandlerInterface $handler)
{
$this->handler = $handler;

ini_set('session.cookie_secure', $this->getCookieSecure());
ini_set('session.cookie_samesite', $this->getCookieSameSite());
ini_set('session.cookie_path', $this->getCookiePath());
}

/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
{
$this->sessionName = $sessionName;
// see https://github.com/symfony/symfony/blob/2adc85d49cbe14e346068fa7e9c2e1f08ab31de6/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/AbstractSessionHandler.php#L35-L37
if (!headers_sent() && !ini_get('session.cache_limiter') && '0' !== ini_get('session.cache_limiter')) {
header(sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) ini_get('session.cache_expire')));
}

return $this->handler->open($savePath, $sessionName);
}

/**
* {@inheritdoc}
*/
protected function doRead($sessionId)
{
return $this->handler->read($sessionId);
}

/**
* {@inheritdoc}
*/
public function updateTimestamp($sessionId, $data)
{
return $this->write($sessionId, $data);
}

/**
* {@inheritdoc}
*/
protected function doWrite($sessionId, $data)
{
return $this->handler->write($sessionId, $data);
}

/**
* {@inheritdoc}
* @see https://github.com/symfony/symfony/blob/2adc85d49cbe14e346068fa7e9c2e1f08ab31de6/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/AbstractSessionHandler.php#L126-L167
*/
public function destroy($sessionId)
{
if (\PHP_VERSION_ID < 70000) {
$this->prefetchData = null;
}
if (!headers_sent() && filter_var(ini_get('session.use_cookies'), FILTER_VALIDATE_BOOLEAN)) {
if (!$this->sessionName) {
throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', \get_class($this)));
}
$sessionCookie = sprintf(' %s=', urlencode($this->sessionName));
$sessionCookieWithId = sprintf('%s%s;', $sessionCookie, urlencode($sessionId));
$sessionCookieFound = false;
$otherCookies = [];
foreach (headers_list() as $h) {
if (0 !== stripos($h, 'Set-Cookie:')) {
continue;
}
if (11 === strpos($h, $sessionCookie, 11)) {
$sessionCookieFound = true;

if (11 !== strpos($h, $sessionCookieWithId, 11)) {
$otherCookies[] = $h;
}
} else {
$otherCookies[] = $h;
}
}
if ($sessionCookieFound) {
header_remove('Set-Cookie');
foreach ($otherCookies as $h) {
header($h, false);
}
} else {
if (\PHP_VERSION_ID < 70300) {
setcookie($this->sessionName, '', 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), filter_var(ini_get('session.cookie_secure'), FILTER_VALIDATE_BOOLEAN), filter_var(ini_get('session.cookie_httponly'), FILTER_VALIDATE_BOOLEAN));
} else {
setcookie($this->sessionName, '',
[
'expires' => 0,
'path' => $this->getCookiePath(),
'domain' => ini_get('session.cookie_domain'),
'secure' => filter_var(ini_get('session.cookie_secure'), FILTER_VALIDATE_BOOLEAN),
'httponly' => filter_var(ini_get('session.cookie_httponly'), FILTER_VALIDATE_BOOLEAN),
'samesite' => $this->getCookieSameSite(),
]
);
}
}
}

return $this->newSessionId === $sessionId || $this->doDestroy($sessionId);
}

/**
* {@inheritdoc}
*/
protected function doDestroy($sessionId)
{
$this->doDestroy = false;

return $this->handler->destroy($sessionId);
}

/**
* {@inheritdoc}
*/
public function close()
{
return $this->handler->close();
}

/**
* @return bool
*/
public function gc($maxlifetime)
{
return $this->handler->gc($maxlifetime);
}

/**
* @return string
*/
public function getCookieSameSite()
{
if ($this->shouldSendSameSiteNone() && \PHP_VERSION_ID >= 70300 && $this->getCookieSecure()) {
return Cookie::SAMESITE_NONE;
}

return '';
}

/**
* @return string
*/
public function getCookiePath()
{
$cookiePath = env('ECCUBE_COOKIE_PATH', '/');
if ($this->shouldSendSameSiteNone() && \PHP_VERSION_ID < 70300 && $this->getCookieSecure()) {
return $cookiePath.'; SameSite='.Cookie::SAMESITE_NONE;
}

return $cookiePath;
}

/**
* @return string
*/
public function getCookieSecure()
{
$request = Request::createFromGlobals();
return $request->isSecure() ? '1' : '0';
}

/**
* @return bool
*/
private function shouldSendSameSiteNone()
{
$userAgent = array_key_exists('HTTP_USER_AGENT', $_SERVER) ? $_SERVER['HTTP_USER_AGENT'] : null;
return SameSite::handle($userAgent);
}
}