feature #28446 [SecurityBundle] make remember-me cookies auto-secure + inherit their default config from framework.session.cookie_* (nicolas-grekas)
This PR was merged into the 4.2-dev branch.
Discussion
----------
[SecurityBundle] make remember-me cookies auto-secure + inherit their default config from framework.session.cookie_*
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | #28338
| License | MIT
| Doc PR | -
Let's make it easier to have a good default security level, now for the remember-me cookie.
Commits
-------
6ec223bf6f
[SecurityBundle] make remember-me cookies auto-secure + inherit their default config from framework.session.cookie_*
This commit is contained in:
commit
10df10ce38
@ -7,7 +7,7 @@ CHANGELOG
|
||||
* added `ProcessorInterface`: an optional interface to allow autoconfiguration of Monolog processors
|
||||
* The methods `DebugProcessor::getLogs()`, `DebugProcessor::countErrors()`, `Logger::getLogs()`
|
||||
and `Logger::countErrors()` will have a new `$request` argument in version 5.0, not defining
|
||||
it is deprecated since Symfony 4.2.
|
||||
it is deprecated
|
||||
|
||||
4.1.0
|
||||
-----
|
||||
|
@ -6,11 +6,13 @@ CHANGELOG
|
||||
|
||||
* Using the `security.authentication.trust_resolver.anonymous_class` and
|
||||
`security.authentication.trust_resolver.rememberme_class` parameters to define
|
||||
the token classes is deprecated. To use
|
||||
custom tokens extend the existing `Symfony\Component\Security\Core\Authentication\Token\AnonymousToken`
|
||||
the token classes is deprecated. To use custom tokens extend the existing
|
||||
`Symfony\Component\Security\Core\Authentication\Token\AnonymousToken`.
|
||||
or `Symfony\Component\Security\Core\Authentication\Token\RememberMeToken`.
|
||||
* Added `Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass`
|
||||
* Added `json_login_ldap` authentication provider to use LDAP authentication with a REST API.
|
||||
* Made remember-me cookies inherit their default config from `framework.session.cookie_*`
|
||||
and added an "auto" mode to their "secure" config option to make them secure on HTTPS automatically.
|
||||
|
||||
4.1.0
|
||||
-----
|
||||
|
@ -15,6 +15,7 @@ use Symfony\Component\Config\Definition\Builder\NodeDefinition;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
|
||||
class RememberMeFactory implements SecurityFactoryInterface
|
||||
{
|
||||
@ -140,7 +141,11 @@ class RememberMeFactory implements SecurityFactoryInterface
|
||||
;
|
||||
|
||||
foreach ($this->options as $name => $value) {
|
||||
if (\is_bool($value)) {
|
||||
if ('secure' === $name) {
|
||||
$builder->enumNode($name)->values(array(true, false, 'auto'))->defaultValue('auto' === $value ? null : $value);
|
||||
} elseif ('samesite' === $name) {
|
||||
$builder->enumNode($name)->values(array(null, Cookie::SAMESITE_LAX, Cookie::SAMESITE_STRICT))->defaultValue($value);
|
||||
} elseif (\is_bool($value)) {
|
||||
$builder->booleanNode($name)->defaultValue($value);
|
||||
} else {
|
||||
$builder->scalarNode($name)->defaultValue($value);
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Bundle\SecurityBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
|
||||
use Symfony\Bundle\SecurityBundle\SecurityUserValueResolver;
|
||||
@ -22,6 +23,7 @@ use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
||||
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
|
||||
use Symfony\Component\DependencyInjection\Parameter;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
@ -37,7 +39,7 @@ use Symfony\Component\Security\Http\Controller\UserValueResolver;
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*/
|
||||
class SecurityExtension extends Extension
|
||||
class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
{
|
||||
private $requestMatchers = array();
|
||||
private $expressions = array();
|
||||
@ -54,6 +56,32 @@ class SecurityExtension extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function prepend(ContainerBuilder $container)
|
||||
{
|
||||
$rememberMeSecureDefault = false;
|
||||
$rememberMeSameSiteDefault = null;
|
||||
|
||||
if (!isset($container->getExtensions()['framework'])) {
|
||||
return;
|
||||
}
|
||||
foreach ($container->getExtensionConfig('framework') as $config) {
|
||||
if (isset($config['session'])) {
|
||||
$rememberMeSecureDefault = $config['session']['cookie_secure'] ?? $rememberMeSecureDefault;
|
||||
$rememberMeSameSiteDefault = array_key_exists('cookie_samesite', $config['session']) ? $config['session']['cookie_samesite'] : $rememberMeSameSiteDefault;
|
||||
}
|
||||
}
|
||||
foreach ($this->listenerPositions as $position) {
|
||||
foreach ($this->factories[$position] as $factory) {
|
||||
if ($factory instanceof RememberMeFactory) {
|
||||
\Closure::bind(function () use ($rememberMeSecureDefault, $rememberMeSameSiteDefault) {
|
||||
$this->options['secure'] = $rememberMeSecureDefault;
|
||||
$this->options['samesite'] = $rememberMeSameSiteDefault;
|
||||
}, $factory, $factory)();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function load(array $configs, ContainerBuilder $container)
|
||||
{
|
||||
if (!array_filter($configs)) {
|
||||
|
@ -26,6 +26,7 @@ class LogoutTest extends WebTestCase
|
||||
$cookieJar->expire(session_name());
|
||||
|
||||
$this->assertNotNull($cookieJar->get('REMEMBERME'));
|
||||
$this->assertSame('lax', $cookieJar->get('REMEMBERME')->getSameSite());
|
||||
|
||||
$client->request('GET', '/logout');
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
imports:
|
||||
- { resource: ./../config/framework.yml }
|
||||
|
||||
framework:
|
||||
session:
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
|
||||
security:
|
||||
encoders:
|
||||
Symfony\Component\Security\Core\User\User: plaintext
|
||||
|
@ -28,7 +28,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/asset": "~3.4|~4.0",
|
||||
"symfony/browser-kit": "~3.4|~4.0",
|
||||
"symfony/browser-kit": "~4.2",
|
||||
"symfony/console": "~3.4|~4.0",
|
||||
"symfony/css-selector": "~3.4|~4.0",
|
||||
"symfony/dom-crawler": "~3.4|~4.0",
|
||||
@ -48,6 +48,7 @@
|
||||
"twig/twig": "~1.34|~2.4"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/browser-kit": "<4.2",
|
||||
"symfony/var-dumper": "<3.4",
|
||||
"symfony/event-dispatcher": "<3.4",
|
||||
"symfony/framework-bundle": "<4.2",
|
||||
|
@ -5,7 +5,8 @@ CHANGELOG
|
||||
-----
|
||||
|
||||
* The method `Client::submit()` will have a new `$serverParameters` argument
|
||||
in version 5.0, not defining it is deprecated since version 4.2
|
||||
in version 5.0, not defining it is deprecated
|
||||
* Added ability to read the "samesite" attribute of cookies using `Cookie::getSameSite()`
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
|
@ -40,6 +40,7 @@ class Cookie
|
||||
protected $secure;
|
||||
protected $httponly;
|
||||
protected $rawValue;
|
||||
private $samesite;
|
||||
|
||||
/**
|
||||
* Sets a cookie.
|
||||
@ -52,8 +53,9 @@ class Cookie
|
||||
* @param bool $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client
|
||||
* @param bool $httponly The cookie httponly flag
|
||||
* @param bool $encodedValue Whether the value is encoded or not
|
||||
* @param string|null $samesite The cookie samesite attribute
|
||||
*/
|
||||
public function __construct(string $name, ?string $value, string $expires = null, string $path = null, string $domain = '', bool $secure = false, bool $httponly = true, bool $encodedValue = false)
|
||||
public function __construct(string $name, ?string $value, string $expires = null, string $path = null, string $domain = '', bool $secure = false, bool $httponly = true, bool $encodedValue = false, string $samesite = null)
|
||||
{
|
||||
if ($encodedValue) {
|
||||
$this->value = urldecode($value);
|
||||
@ -67,6 +69,7 @@ class Cookie
|
||||
$this->domain = $domain;
|
||||
$this->secure = $secure;
|
||||
$this->httponly = $httponly;
|
||||
$this->samesite = $samesite;
|
||||
|
||||
if (null !== $expires) {
|
||||
$timestampAsDateTime = \DateTime::createFromFormat('U', $expires);
|
||||
@ -106,6 +109,10 @@ class Cookie
|
||||
$cookie .= '; httponly';
|
||||
}
|
||||
|
||||
if (null !== $this->samesite) {
|
||||
$str .= '; samesite='.$this->samesite;
|
||||
}
|
||||
|
||||
return $cookie;
|
||||
}
|
||||
|
||||
@ -138,6 +145,7 @@ class Cookie
|
||||
'secure' => false,
|
||||
'httponly' => false,
|
||||
'passedRawValue' => true,
|
||||
'samesite' => null,
|
||||
);
|
||||
|
||||
if (null !== $url) {
|
||||
@ -186,7 +194,8 @@ class Cookie
|
||||
$values['domain'],
|
||||
$values['secure'],
|
||||
$values['httponly'],
|
||||
$values['passedRawValue']
|
||||
$values['passedRawValue'],
|
||||
$values['samesite']
|
||||
);
|
||||
}
|
||||
|
||||
@ -298,4 +307,14 @@ class Cookie
|
||||
{
|
||||
return null !== $this->expires && 0 != $this->expires && $this->expires < time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the samesite attribute of the cookie.
|
||||
*
|
||||
* @return string|null The cookie samesite attribute
|
||||
*/
|
||||
public function getSameSite(): ?string
|
||||
{
|
||||
return $this->samesite;
|
||||
}
|
||||
}
|
||||
|
@ -202,4 +202,13 @@ class CookieTest extends TestCase
|
||||
{
|
||||
$cookie = new Cookie('foo', 'bar', 'string');
|
||||
}
|
||||
|
||||
public function testSameSite()
|
||||
{
|
||||
$cookie = new Cookie('foo', 'bar');
|
||||
$this->assertNull($cookie->getSameSite());
|
||||
|
||||
$cookie = new Cookie('foo', 'bar', 0, '/', 'foo.com', false, true, false, 'lax');
|
||||
$this->assertSame('lax', $cookie->getSameSite());
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ CHANGELOG
|
||||
* The `$currentUri` constructor argument of the `AbstractUriElement`, `Link` and
|
||||
`Image` classes is now optional.
|
||||
* The `Crawler::children()` method will have a new `$selector` argument in version 5.0,
|
||||
not defining it is deprecated since version 4.2.
|
||||
not defining it is deprecated.
|
||||
|
||||
3.1.0
|
||||
-----
|
||||
|
@ -5,8 +5,8 @@ CHANGELOG
|
||||
-----
|
||||
|
||||
* added $useNaturalSort option to Finder::sortByName() method
|
||||
* The `Finder::sortByName()` method will have a new `$useNaturalSort`
|
||||
argument in version 5.0, not defining it is deprecated since version 4.2.
|
||||
* the `Finder::sortByName()` method will have a new `$useNaturalSort`
|
||||
argument in version 5.0, not defining it is deprecated
|
||||
|
||||
4.0.0
|
||||
-----
|
||||
|
@ -165,7 +165,8 @@ class RequestDataCollector extends DataCollector implements EventSubscriberInter
|
||||
'controller' => $this->parseController($request->attributes->get('_controller')),
|
||||
'status_code' => $statusCode,
|
||||
'status_text' => Response::$statusTexts[(int) $statusCode],
|
||||
))
|
||||
)),
|
||||
0, '/', null, $request->isSecure(), true, false, 'lax'
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -225,6 +225,8 @@ class RequestDataCollectorTest extends TestCase
|
||||
$cookie = $this->getCookieByName($response, 'sf_redirect');
|
||||
|
||||
$this->assertNotEmpty($cookie->getValue());
|
||||
$this->assertSame('lax', $cookie->getSameSite());
|
||||
$this->assertFalse($cookie->isSecure());
|
||||
}
|
||||
|
||||
public function testItCollectsTheRedirectionAndClearTheCookie()
|
||||
|
@ -271,7 +271,7 @@ abstract class AbstractRememberMeServices implements RememberMeServicesInterface
|
||||
$this->logger->debug('Clearing remember-me cookie.', array('name' => $this->options['name']));
|
||||
}
|
||||
|
||||
$request->attributes->set(self::COOKIE_ATTR_NAME, new Cookie($this->options['name'], null, 1, $this->options['path'], $this->options['domain'], $this->options['secure'], $this->options['httponly'], false, $this->options['samesite'] ?? null));
|
||||
$request->attributes->set(self::COOKIE_ATTR_NAME, new Cookie($this->options['name'], null, 1, $this->options['path'], $this->options['domain'], $this->options['secure'] ?? $request->isSecure(), $this->options['httponly'], false, $this->options['samesite'] ?? null));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,7 +83,7 @@ class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices
|
||||
time() + $this->options['lifetime'],
|
||||
$this->options['path'],
|
||||
$this->options['domain'],
|
||||
$this->options['secure'],
|
||||
$this->options['secure'] ?? $request->isSecure(),
|
||||
$this->options['httponly'],
|
||||
false,
|
||||
$this->options['samesite'] ?? null
|
||||
@ -118,7 +118,7 @@ class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices
|
||||
time() + $this->options['lifetime'],
|
||||
$this->options['path'],
|
||||
$this->options['domain'],
|
||||
$this->options['secure'],
|
||||
$this->options['secure'] ?? $request->isSecure(),
|
||||
$this->options['httponly'],
|
||||
false,
|
||||
$this->options['samesite'] ?? null
|
||||
|
@ -80,7 +80,7 @@ class TokenBasedRememberMeServices extends AbstractRememberMeServices
|
||||
$expires,
|
||||
$this->options['path'],
|
||||
$this->options['domain'],
|
||||
$this->options['secure'],
|
||||
$this->options['secure'] ?? $request->isSecure(),
|
||||
$this->options['httponly'],
|
||||
false,
|
||||
$this->options['samesite'] ?? null
|
||||
|
Reference in New Issue
Block a user