3

I've been doing my own CSRF protection using PHP. From what I've read I decided to use a cookie to implement my protection but feel a little confused as to whether my method is secure against CSRF attacks.

So my method follows:

  1. User sends request to login

  2. Server checks if a CSRF token is set, if not create one and store it in their Session and create a Cookie with the token as well

  3. Validate the CSRF token through checking if it is in the POST request, if not then check for the token in $_COOKIE

  4. Send message back if token is invalid...

I decided to use a cookie to store the token as this will work for Ajax requests and I won't have to include it every time I use an Ajax POST.

What I am confused about is couldn't an attacker just make a request; POST or GET and because the cookie is there it just gets sent with the request anyway, thus being a valid request as the token is sent with the browser every time?

Erdss4
  • 1,025
  • 3
  • 11
  • 31
  • [CodeReview](https://codereview.stackexchange.com) would be better suited for you, I believe. – Script47 Sep 27 '17 at 13:42
  • Browsers will block cross-site requests so any request which has a valid CSRF token in a cookie is normally from a client browsing your own domain (unless you enable CORS in some routes, in that case you need to beware). – apokryfos Sep 27 '17 at 14:00
  • https://stackoverflow.com/a/20518324/487813 is a good read for this – apokryfos Sep 27 '17 at 14:02
  • @apokryfos no , browser do not block cross-site request in general. They block requests where you expect to get data back. A `XMLHttpRequest` uses CORS. But a hidden `
    ` that is submitted using JavaScript, can still send a POST request to a foreign domain. And such request will include all cookies of the user. Storing a CSRF in a Cookie is as if you don't have a CSRF token.
    – t.niese Sep 27 '17 at 14:24
  • @t.niese A compliant browser must block Cross origin requests unless they pass certain strict rules (see section 7.2 of https://www.w3.org/TR/cors). Specifically: *If the response includes zero or more than one Access-Control-Allow-Origin header values, return fail and terminate this algorithm.* . Whether the `Access-Control-Allow-Origin` header is set is entirely a developer decision. A possible exception of this is JSONP requests. – apokryfos Sep 27 '17 at 14:31
  • @apokryfos CSRF exits to protect requests from browser that do not respect CORS in general, or for requests like form `POST`/`GET` and script loading (jsonp) that are allowed by all current browsers (if this is according to the specs does not matter). If all browsers would respect CORS and if cross origin form `POST` would not be allowed by those specs, then CSRF would not be relevant. But because cross domain `POST` with forms is possible, CSRF tokens are required, and they will only have any additional security over session cookies, if they are not stored in a cookie on the client. – t.niese Sep 27 '17 at 15:26
  • 1
    @apokryfos the relevant part that covers this is `[...]Simple cross-origin requests generated outside this specification (such as cross-origin form submissions using GET or POST or cross-origin GET requests resulting from script elements) typically include user credentials, so resources conforming to this specification must always be prepared to expect simple cross-origin requests with credentials.[...]` ([4 Security Considerations](https://www.w3.org/TR/cors/#security)) – t.niese Sep 27 '17 at 15:35

2 Answers2

4

The cookie should not contain the CSRF token. Only the client side session storage should contain it. And you should not against a CSRF that is in the cookie.

If you would check a CSRF that is send with the cookie you would bypass the idea behind CSRF.

A simple attack scenario would be a hidden form on a foreign website:

<form method="method" action="http://site-to-gain-access-to.tld/someactionurl">
  <!-- some form data -->
</form>

And this hidden form will be executed using javascript without the user's intervention. If the user is logged in on the site site-to-gain-access-to.tld and if there is no CSRF protection active then it would be like if the user itself would have triggered that action, because the session cookies of the user will be send with that request. So the server would assume that it was the user that triggered that request.

If you now would place the CSRF token in your cookie you would have the same problem as with a session.

The CSRF token has to be always sended only as part of the request body or url, and not as cookie.

So the the highlighted parts:

Server checks if a CSRF token is set, if not create one and store it in their Session and create a Cookie with the token as well

Validate the CSRF token through checking if it is in the POST request, if not then check for the token in $_COOKIE

would break the CSRF protection. It does not matter if the CSRF is stored in the cookies as plaintext or with a server side encryption.

Community
  • 1
  • 1
t.niese
  • 39,256
  • 9
  • 74
  • 101
  • Right, ok I think that clears it up for me. If I'm correct then, including the CSRF token only as a hidden input in the form I want to send and then comparing it against the token in the session on the server is enough to prevent CSRF attacks? – Erdss4 Sep 27 '17 at 14:13
  • 1
    Yes you should only include it as hidden input in the form and compare it against the server side session storage. It is one part to make your site secure. You for sure need to make sure that your site is save again XSS attack, because otherwise a malicious script might be able to gain access that CSRF token. So the session id confirms that the request was done by the browser of the user, and the CSRF confirms that the request was done by the user. – t.niese Sep 27 '17 at 14:16
  • Ok that makes sense. But for Ajax requests, is it still secure to include the CSRF token which is located in a div on the page, as some of my Ajax requests are not from forms being POSTed? – Erdss4 Sep 27 '17 at 14:23
  • 1
    @Erdss4 Yes you can pass the CSRF token with the ajax request, as a property in the request body, or as a header with an `x-` header like `X-CSRF-Token`. Personally would use an `x-` header because it would not interfere with your request data. – t.niese Sep 27 '17 at 14:28
  • In your webpage you could add the token e.g. as a `` and then you can access that token at each request you do. – t.niese Sep 27 '17 at 14:32
  • A little off topic but what is it called when you add "data-" to elements like that? – Erdss4 Sep 27 '17 at 16:07
  • 1
    @Erdss4 you add an [data attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes#attr-dataset) to an html element ([Using data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes)). Those can be directly access in modern browsers using `nodeElement.dataset` or are accessible in libraries like: [jQuery.data()](https://api.jquery.com/jquery.data/) – t.niese Sep 27 '17 at 16:15
2

As long as "User sends request to login" refers to the initial GET request for the login page, this is correct. The CSRF should be generated and stored on the GET request and validated during every POST.

What I am confused about is couldn't an attacker just make a request; POST or GET and because the cookie is there it just gets sent with the request anyway, thus being a valid request as the token is sent with the browser every time?

Yes. CSRF tokens are not secret. They simply confirm that a POST is made only after making an expected GET request. Meaning someone can only submit a form if they requested the form in the first place. It prevents an bad site from sending a user across to your site with a POST. It does not prevent an attacker from making a GET request, grabbing a token, and making a valid POST.

From OWASP:

With a little help of social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker's choosing. If the victim is a normal user, a successful CSRF attack can force the user to perform state changing requests.

Matt S
  • 14,976
  • 6
  • 57
  • 76
  • So if I were to just include my token as a hidden input within every form and validate the token with the one stored in the SESSION variable, that is not enough? Are you saying the token should also be part of the GET request in the forms action? – Erdss4 Sep 27 '17 at 13:57
  • Including the token in a hidden input element is enough (as long as your site doesn't change data on GET requests). I was explaining that the token is generated during a GET request (to retrieve the page with the form). You can use a cookie or hidden input element to then submit the token again with the POST. – Matt S Sep 27 '17 at 14:00
  • So could you just ignore putting a hidden input in a form and just rely on the cookie being sent every time? – Erdss4 Sep 27 '17 at 14:04
  • Cookies require some extra considerations. I see them listed in other people's comments and posts. Also read the related pages from the OWASP link I provided. – Matt S Sep 27 '17 at 14:43