-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Security][CSRF] Double Submit Cookies CSRF prevention strategy #18333
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
Changes from 1 commit
d746f31
e9573af
beed130
1cea0d2
a298be6
a533b0e
893fa0c
61b5483
6e19ef9
99dde3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Security\Core\Exception; | ||
|
||
/** | ||
* Base UnexpectedValueException for the Security component. | ||
* | ||
* @author Oliver Hoff <oliver@hofff.com> | ||
*/ | ||
class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface | ||
{ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ | |
|
||
use Symfony\Component\HttpFoundation\Cookie; | ||
use Symfony\Component\HttpFoundation\ParameterBag; | ||
use Symfony\Component\Security\Core\Exception\InvalidArgumentException; | ||
use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException; | ||
|
||
/** | ||
|
@@ -27,7 +28,7 @@ class CookieTokenStorage implements TokenStorageInterface | |
/** | ||
* @var array | ||
8000 */ | ||
private $transientTokens; | ||
private $transientTokens = array(); | ||
|
||
/** | ||
* @var ParameterBag | ||
|
@@ -45,7 +46,6 @@ class CookieTokenStorage implements TokenStorageInterface | |
*/ | ||
public function __construct(ParameterBag $cookies, $secure) | ||
{ | ||
$this->transientTokens = []; | ||
$this->cookies = $cookies; | ||
$this->secure = (bool) $secure; | ||
} | ||
|
@@ -58,8 +58,8 @@ public function getToken($tokenId) | |
{ | ||
$token = $this->resolveToken($tokenId); | ||
|
||
if (!strlen($token)) { | ||
throw new TokenNotFoundException; | ||
if ('' === $token) { | ||
throw new TokenNotFoundException(); | ||
} | ||
|
||
return $token; | ||
|
@@ -71,7 +71,7 @@ public function getToken($tokenId) | |
*/ | ||
public function hasToken($tokenId) | ||
{ | ||
return strlen($this->resolveToken($tokenId)) > 0; | ||
return '' !== $this->resolveToken($tokenId); | ||
} | ||
|
||
/** | ||
|
@@ -80,8 +80,10 @@ public function hasToken($tokenId) | |
*/ | ||
public function setToken($tokenId, $token) | ||
{ | ||
if (!strlen($token)) { | ||
throw new \InvalidArgumentException('Empty tokens are not allowed'); | ||
$token = (string) $token; | ||
|
||
if ('' === $token) { | ||
8000 throw new InvalidArgumentException('Empty tokens are not allowed'); | ||
} | ||
|
||
$this->updateToken($tokenId, $token); | ||
|
@@ -97,7 +99,7 @@ public function removeToken($tokenId) | |
|
||
$this->updateToken($tokenId, ''); | ||
|
||
return strlen($token) ? $token : null; | ||
return '' === $token ? null : $token; | ||
} | ||
|
||
/** | ||
|
@@ -113,7 +115,6 @@ public function createCookies() | |
// the problem is the that the value of deleted cookies get set to | ||
// the string "deleted" and not the empty string | ||
|
||
// TODO http only? | ||
$name = $this->generateCookieName($tokenId); | ||
$cookies[] = new Cookie($name, $token, 0, null, null, $this->secure, true); | ||
} | ||
|
@@ -143,7 +144,6 @@ protected function resolveToken($tokenId) | |
*/ | ||
protected function updateToken($tokenId, $token) | ||
{ | ||
$token = (string) $token; | ||
$name = $this->generateCookieName($tokenId); | ||
|
||
if ($token === $this->cookies->get($name, '')) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't it be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The empty string token is the non-existant token. Thats because we dont have an explicit "delete" cookie header. If you want to delete a cookie, you need to overwrite a previously existing one. Thats also why it is disallowed to pass an empty string token to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,8 @@ | |
namespace Symfony\Component\Security\Csrf\TokenStorage; | ||
|
||
use Symfony\Component\HttpFoundation\RequestStack; | ||
use Symfony\Component\Security\Core\Exception\RuntimeException; | ||
use Symfony\Component\Security\Core\Exception\UnexpectedValueException; | ||
|
||
/** | ||
* Forwards token storage calls to a token storage stored in the master | ||
|
@@ -67,18 +69,25 @@ public function getProxiedTokenStorage() | |
$request = $this->requestStack->getMasterRequest(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. master? |
||
|
||
if (!$request) { | ||
throw new \RuntimeException('Not in a request context'); | ||
throw new RuntimeException('Not in a request context'); | ||
} | ||
|
||
$storage = $request->attributes->get($this->tokenStorageKey); | ||
|
||
// TODO what if storage is set and not an instance of TokenStorageInterface? | ||
// error out? overwrite? | ||
if (!$storage instanceof TokenStorageInterface) { | ||
$storage = $this->factory->createTokenStorage($request); | ||
$request->attributes->set($this->tokenStorageKey, $storage); | ||
if ($storage instanceof TokenStorageInterface) { | ||
return $storage; | ||
} | ||
|
||
if ($storage !== null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yoda |
||
throw new UnexpectedValueException(sprintf( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be inlined |
||
'Expected null or an instance of "Symfony\\Component\\Security\\Csrf\\TokenStorage\\TokenStorageInterface", got "%s"', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you could use "%s" with a replacement by |
||
is_object($storage) ? get_class($storage) : gettype($storage) | ||
)); | ||
} | ||
|
||
$storage = $this->factory->createTokenStorage($request); | ||
$request->attributes->set($this->tokenStorageKey, $storage); | ||
|
||
return $storage; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
namespace Symfony\Component\Security\Csrf\TokenStorage; | ||
|
||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\Security\Core\Exception\RuntimeException; | ||
|
||
/** | ||
* Creates CSRF token storages based on the requests session. | ||
|
@@ -30,7 +31,7 @@ class SessionTokenStorageFactory implements TokenStorageFactoryInterface | |
*/ | ||
public function __construct($namespace = null) | ||
{ | ||
$this->namespace = $namespace === null ? SessionTokenStorage::SESSION_NAMESPACE : $namespace; | ||
$this->namespace = $namespace === null ? SessionTokenStorage::SESSION_NAMESPACE : (string) $namespace; | ||
} | ||
|
||
/** | ||
|
@@ -40,7 +41,7 @@ public function __construct($namespace = null) | |
public function createTokenStorage(Request $request) { | ||
$session = $request->getSession(); | ||
if (!$session) { | ||
throw new \RuntimeException('Request has no session'); | ||
throw new RuntimeException('Request has no session'); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could modify the namespace here, to use a different one for SSL connections, because tokens transmitted via non SSL connections are subject to MITM attacks. |
||
return new SessionTokenStorage($session, $this->namespace); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's preventing you to normalize it ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the question is: is the behavior of
Cookie
correct to overwrite a zero length cookie value with the value "deleted" (and a past expiration date). opinions on stackoverflow suggest using the empty string value and a past expiration date to "delete" a cookie. (in general a past expiration date should be enough, but browsers are not required to dismiss expired cookies, so in the case the browser decides to submit the cookie anyway, we must detect it as "deleted". But there is no way to distinguish between a "deleted" cookie and a cookie actually containing the value "deleted".)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the string "deleted" used anywhere else in the code ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would clash, if someone tries to
setToken($tokenId, 'deleted')
(basically in every use case, where the cookie value "deleted" is in the allowed value set of the cookie)