79
votes

Cross Site Request Forgery (CSRF) is typically prevent with one of the following methods:

  • Check referer - RESTful but unreliable
  • insert token into form and store the token in the server session - not really RESTful
  • cryptic one time URIs - not RESTful for the same reason as tokens
  • send password manually for this request (not the cached password used with HTTP auth) - RESTful but not convenient

My idea is to use a user secret, a cryptic but static form id and JavaScript to generate tokens.

<form method="POST" action="/someresource" id="7099879082361234103">
    <input type="hidden" name="token" value="generateToken(...)">
    ...
</form>
  1. GET /usersecret/john_doe fetched by the JavaScript from the authenticated user.
  2. Response: OK 89070135420357234586534346 This secret is conceptionally static, but can be changed every day/hour ... to improve security. This is the only confidential thing.
  3. Read the cryptic (but static for all users!) form id with JavaScript, process it together with the user secret: generateToken(7099879082361234103, 89070135420357234586534346)
  4. Send the form along with the generated token to the server.
  5. Since the server knows the user secret and the form id, it is possible to run the same generateToken function as the client did before sending and compare both results. Only when both values are equal the action will be authorized.

Is something wrong with this approach, despite the fact that it doesn't work without JavaScript?

Addendum:

6
Your usersecret isn't unique to the user, an attacker simply needs to get that number and adjust their scripts to use the new calculation. How are you authenticating users if you have no state at all?Mike
The user secret is unique per user and can only be retrieved after authentication (HTTP basic or digest authentication or certificate authentication)deamon

6 Answers

32
votes

There are a lot of answers here, and problems with quite a few of them.

Things you should NOT do:

  1. If you need to read the session token from JavaScript, you're doing something horribly wrong. Your session identifier cookie should ALWAYS have HTTPOnly set on it so its not available to scripts.

    This one protection makes it so that the impact of XSS is considerably reduced, since an attacker will no longer be able to get a logged in users session token, which for all intents and purposes are the equivalent of credentials in the application. You don't want one error to give keys to the kingdom.

  2. The session identifier should not be written to the contents of the page. This is for the same reasons you set HTTPOnly. This means that that your csrf token can not be your session id. They need to be different values.

Things you should do:

  1. Follow OWASP's guidance:

  2. Specifically, if this is a REST application you can require double-submission of CSRF tokens. If you do this, just be sure that you define it to a specific full-domain (www.mydomain.com) and not a parent domain (example.com), and that you also utilize the "samesite" cookie attribute which is gaining popularity.

Simply create something cryptographically random, store it in ASCII Hex or Base64 encode, and add it as a cookie and to your forms when the server returns the page. On the server side make sure that the cookie value matches the form value. Voila, you've killed CSRF, avoided extra prompts for your users, and not opened yourself up to more vulnerabilities.

NOTE: As @krubo states below the double-submission technique has been found to have some weaknesses (See Double-Submission). Since this weakness requires that:

  1. You define a cookie scoped to the parent domain.
  2. You fail to set HSTS.
  3. The attacker controls some network location inbetween the user and the server

I kind of think the weakness falls more in the category of a "Cool Defcon Talk" rather than a "Realworld Security Risk". In any case, if you are going to use double-submission it doesn't hurt to take a few extra steps to protect yourself fully.


New Update 07/06/2020

My new favorite way to do double-submission is to create and pass a cryptographic random string in the body of the request as before; but rather than have the cookie be the same exact value have the cookie be the encoded value of the string being signed by a certificate. This is still just as easy to validate on the server side, but is MUCH harder for an attacker to mimic. You should still use the samesite Cookie attribute and other protections outlined earlier in my post.

10
votes

Am I getting this right:

  • You want protection against CSRF for users logged in via cookies.
  • And at the same time you want RESTful interface for Basic, OAuth and Digest authenticated requests from apps.

So, why not check whether users is logged in via cookie and apply CSRF only then?

I'm not sure but is possible for another site to forge things like Basic auth or headers?

As far as I know , CSRF is all about cookies? RESTful auth doesn't happen with cookies.

9
votes

You definitely need some state on the server to authenticate/authorize. It need not be the http session though, you could store it in a distributed cache (like memcached) or a database.

If you use cookies for authentication, the easiest solution is to double-submit the cookie value. Before you submit the form, read the session id from the cookie, store it in a hidden field and then submit it. On the server side, confirm that the value in the request is the same as the session id (that you got from the cookie). Evil script from another domain will not be able to read the session id from the cookie, thus preventing CSRF.

This scheme uses a single identifier across the session.

If you want more protection, generate a unique id per-session per-form.

Also, DO NOT generate tokens in JS. Anybody can copy the code and run it from a different domain to attack your site.

6
votes

The static form ID provides no protection at all; an attacker can fetch it himself. Remember, the attacker is not constrained to using JavaScript on the client; he can fetch the static form ID server-side.

I'm not sure I entirely understand the proposed defense; where does the GET /usersecret/john_doe come from? Is that part of the page JavaScript? Is that the literal proposed URL? If so, I'm assuming that username is not a secret, which means that evil.ru can recover user secrets if a browser or plugin bug allows cross-domain GET requests. Why not store the user secret in a cookie upon authentication rather than let anyone who can do cross-domain GETs retrieve it?

I would read "Robust Defenses for Cross-Site Forgery" really carefully before I implemented my own authentication system that I wanted to be resistant to CSRF. In fact, I would reconsider implementing my own authentication system at all.

3
votes

There are a few methods in the CSRF Prevention Cheat Sheet that can be used by restful service. The most RESTful stateless CSRF mitigation is using the Origin or HTTP referer to make sure the requests originate from a domain you trust.

0
votes

Is something wrong with this approach, despite the fact that it doesn't work without JavaScript?

Your user secret is not a secret if you send it to the client. We usually use such secrets to generate hashes and send them with the form, and wait them back for comparison.

If you want to be RESTful, the request has to contain every information about how to process it. The ways you can do this:

  • Add a csrf token cookie with your REST client and send the same token in hidden input with your forms. If the service and the client are under different domains, you have to share the credentials. On the service you have to compare the 2 tokens, and if they are the same, the request is valid...

  • You can add the csrf token cookie with your REST service and send the same token with the representations of your resources (hidden inputs, etc...). Everything else is the same as the end of the previous solution. This solution is on the edge of RESTfulness. (It is okay until the client do not call the service to modify the cookie. If the cookie is http only, the client should not know about it, if it is not, then the client should set it.) You can do a more complex solution if you add different tokens to each forms and add expiration time to the cookies. You can send the expiration time back with the forms as well, so you will know the reason when a token validation fails.

  • You can have a user secret (different by each user) in the resource state on you service. By building representations, you can generate a token (and expiration time) for each form. You can generate a hash from the actual token (and expiration time, method, url, etc...) and the user secret, and send that hash with the form as well. You keep the "user secret" in secret of course, so you never send that with the form. After that if your service gets a request, you can generate the hash from the request parameters and user secret again, and compare them. If the don't match, the request is invalid...

None of them will protect you if your REST client is javascript injectable, so you have to check all your user content against HTML entities, and remove all of them, or use TextNodes always instead of innerHTML. You have to protect yourself against SQL injection and HTTP header injection as well. Never use simple FTP to refresh your site. And so on... There are many ways to inject evil code into your site...

I almost forgot to mention, that GET requests are always for reading by the service and by the client either. By the service this is obvious, by the client setting any url in the browser must result a representation of a resource or multiple resources, it should never call a POST/PUT/DELETE method on a resource. For example GET http://my.client.com/resource/delete -> DELETE http://my.api.com/resource is a very-very bad solution. But this is very basic skill if you want to hinder CSRF.