Skip to content

[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

Closed
wants to merge 10 commits into from

Conversation

backbone87
Copy link
Contributor

@backbone87 backbone87 commented Mar 28, 2016

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass?
Fixed tickets #18313 #13464
License MIT
  • cookie token storage managing csrf tokens stored inside cookies
    (prerequisite for "double submit cookies" csrf prevention strategy)
  • kernel response event listener add the cookie headers of the cookie
    token storage to request responses
  • token storage factory interface to create token storages based on
    requests
  • session token storage factory using the requests session to create a
    session token storage
  • cookie token storage factory using the requests cookies to create a
    cookie token storage
  • request stack token storage managing and using token storages inside
    master request's attributes

assistance on the TODOs and FIXMEs as well as some general feedback is very welcome

*/
public function __construct(ParameterBag $cookies, $secure)
{
$this->transientTokens = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be at variable setting not in constructor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think its personal preference, i try to do all property initialization within constructors and cleanly separate it from property declaration.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be set on properties in symfony core.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HeahDude is right. And you should use array() instead of [].

@Koc
Copy link
Contributor

Koc commented Mar 29, 2016

closes #13464

@acasademont
Copy link
Contributor

Any progress on this one?

{
/**
* {@inheritDoc}
* @see \Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface::getToken()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the @see tag should be removed everywhere as it does not add anything useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can remove it, but i disagree that it doesnt add anything useful: you see where the original declaration comes from, without doin any clicks in your IDE. you only see, if its an overridden method in IDEs, not who the original definer was (without doin more clicks for inspection). also on github you dont have any code assist regarding inheritance.

@Koc
Copy link
Contributor

Koc commented Sep 20, 2016

@backbone87 can you update PR according to suggested changes?

- cookie token storage managing csrf tokens stored inside cookies
(prerequisite for "double submit cookies" csrf prevention strategy)
- kernel response event listener add the cookie headers of the cookie
token storage to request responses
- token storage factory interface to create token storages based on
requests
- session token storage factory using the requests session to create a
session token storage
- cookie token storage factory using the requests cookies to create a
cookie token storage
- request stack token storage managing and using token storages inside
master request's attributes
@backbone87
Copy link
Contributor Author

I have updated the code. There are still open questions, pls check TODOs and FIXMEs.

Also #18115 is required to be resolved, before this can be used, or we start spamming the user with cookies.

/**
* {@inheritdoc}
*
* @see \Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface::getToken()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all those @see should be removed, they do not add anything useful (the class implements TokenStorageInterface).

if (!$session) {
throw new RuntimeException('Request has no session');
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

@backbone87
Copy link
Contributor Author

Ok, I have read a little bit more about DSC method, and I need help regarding determining the correct way to implement its usage within the form component.

AFAIU you dont include the token value in a hidden field within the form, but are required that client side JS reads the cookie value and puts it into a hidden form field before posting the form. If you would include the token value when generating the form on the server then someone could just GET the form from a cross-site (unless you have CORS preventing JS GETs on the form HTML),

This fact basically eliminates the use of DSC as the default method of Symfony, because you rely on client side JS execution.

@backbone87
Copy link
Contributor Author

I have modified the CookieTokenStorage to use a second verification cookie to fix some shortcommings of the simple DSC method http://security.stackexchange.com/a/61039
the use of a verification cookie basically prevents the injection of client generated cookies, when you dont have full subdomain control.

also the tokens now have a configurable TTL to avoid permanent tokens when using session continuation in clients. the lifetime gets refreshed each time the token is retrieved while having less than half of its TTL left.

i also created a SE fork with a manual configuration for end-to-end test of this PR: https://github.com/backbone87/symfony-standard/tree/test/ticket-18313
a security review should be done on this demo

@backbone87
Copy link
Contributor Author

I have reworked the CookieTokenStorage again. Its back to using one cookie (basically just the verification cookie left). How the token itself is passed to the client for resubmission in POST or Header data is subject to the developer (this is in line with the current CSRF workflow).

The CookieTokenStorage now works on the raw HTTP Cookie request header. This is required to close certain attack vectors (overwriting existing cookies by applying a more specific path). Also the cookie name has a nonce suffix that is included in the signature, which makes it a lot harder to inject cookies obtained by the attacker in his own session beforehand.

@backbone87
Copy link
Contributor Author

i cant finish this PR without review and feedback

Copy link
Contributor

@HeahDude HeahDude left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry my comments are only about style, but I can't do more for now :)

/**
* {@inheritdoc}
*/
public function getToken($tokenId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it makes sense to mark concrete public methods as final?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think final should only be used in very special cases when you absolutely rely on the implemented behavior and to keep the internal state of the class clean and valid, and this cannot be provided by subclasses (e.g. not being able to access private properties). Both is not the case here. Imagine someone wants to create a decorater that fixes the token ID to a certain value, using this class as a parent would not be possible with final methods.

$this->cookies = self::parseCookieHeader($cookies);
$this->secure = (bool) $secure;
$this->secret = (string) $secret;
$this->ttl = $ttl === null ? 60 * 60 : (int) $ttl;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null === $ttl

}
}

if ($token !== '') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'' !== $token

public static function getSubscribedEvents()
{
return array(
KernelEvents::RESPONSE => array(array('onKernelResponse', 0)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 is needless

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure, if this will be the actual priority used in the end.

public function __construct($secret, $ttl = null)
{
$this->secret = (string) $secret;
$this->ttl = $ttl === null ? 60 * 60 : (int) $ttl;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yoda style :)

* @param TokenStorageFactoryInterface $factory
* @param string|null $tokenStorageKey
*/
public function __construct(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please inline the arguments for consistency with the code base

public function getProxiedTokenStorage()
{
// TODO use master or current request?
$request = $this->requestStack->getMasterRequest();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

master?

return $storage;
}

if ($storage !== null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yoda

}

if ($storage !== null) {
throw new UnexpectedValueException(sprintf(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be inlined


if ($storage !== null) {
throw new UnexpectedValueException(sprintf(
'Expected null or an instance of "Symfony\\Component\\Security\\Csrf\\TokenStorage\\TokenStorageInterface", got "%s"',
Copy link
Contributor

@HeahDude HeahDude Sep 27, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could use "%s" with a replacement by TokenStorageInterface::class. Also the message should say implementation instead of instance since this is an interface

@nicolas-grekas nicolas-grekas changed the title [WIP][Security][CSRF] Double Submit Cookies CSRF prevention strategy [Security][CSRF] Double Submit Cookies CSRF prevention strategy Dec 6, 2016
@nicolas-grekas nicolas-grekas added this to the 3.x milestone Dec 6, 2016
@nicolas-grekas
Copy link
Member

@backbone87 what's the status here?

@weaverryan
Copy link
Member

Someone with some knowledge of the Double Submit cookie CSRF protection needs to help review this. It looks like @backbone87 has put a lot of good work into it, but we haven't reviewed this yet on real, technical merit. But since feature freeze for 3.4 is only a few days away, we might not have time now.

@backbone87 In your opinion, is that accurate? Basically, this PR is done from your point-of-view, and is just waiting on an opinion from core members?

@backbone87
Copy link
Contributor Author

backbone87 commented Sep 26, 2017

@weaverryan yes thats right for the main part, but there are other things that need to be done

this PR contains the pure infrastructure and handling logic to supporting DSC CSRF

what needs to be done:

@backbone87
Copy link
Contributor Author

backbone87 commented Sep 27, 2017

Some more information on this, after i digged into this PR again. This should act like a "secure" storage inside browsers cookies.

The cookies are marked HTTP-only and should not be able to be read by any client side scripts. The cookies have (partially) randomized names, contain the token_id, an expiry date (not to be confused with the cookie expiration date, which is always client session bound) and are signed with a server secret.

The token id can be used inside hidden form fields.

Security considerations:

  • An attacker can not create a fake cookie (server signature)
  • An attacker can not read a stored cookie (HTTP only)
    • Possible attack vector for older browsers: Using an XmlHttpRequest to obtain the cookie (some older browser do not hide away HTTP only cookies in AJAX requests)
  • An attacker can not obtain a token value from a generated form (SOP/CORS) (Same attack vectors as synchronizer pattern)
    • Possible attack vector for older browsers that do not properly support SOP/CORS
    • Possible attack vector for missconfigured CORS
  • An attacker can not inject a 2nd cookie that he obtained previously from the server (from a 3rd party client)
    • HERE COMES THE REAL PROBLEM I NEED VERIFICATION/SOLUTION FOR: If the attacker has obtained a signed cookie (via 3rd party client -> easy) and injects it somehow into the sites domain (idk how this works without XSS) and the client does not contain another csrf cookie already (because the client didnt visit a page of the website before that used a csrf cookie), then he is able to CSRF. idk if all these conditions can be met

edit: one way to inject cookies in the client even if your site doesnt have XSS vectors, is if you do not have full domain control. e.g. there are subdomains that can be controlled by an attacker

@weaverryan weaverryan modified the milestones: 3.4, 4.1 Sep 27, 2017
@weaverryan
Copy link
Member

Thanks for the summary @backbone87 :). I'd like this to move forward, so we can find out whether it's a "sound" idea and merge it or not. But, this will not happen for 3.4 - it would be too rushed. I've changed the milestone to 4.1.

@nicolas-grekas nicolas-grekas modified the milestones: 4.1, next Apr 20, 2018
@MatTheCat
Copy link
Contributor

Is this PR still alive?

@fabpot
Copy link
Member

fabpot commented Jul 8, 2019

Closing as there is no more activities here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.