March 5th, 2007 by rick

Your requests are safe with us

8 comments on 1298 words

Sometime during the development of Lighthouse I did a bit of reading on web security issues, specifically CSRF attacks. CSRF attacks were a bit tough to grasp at first, because that’s just how the web works. Basically, a page can make a request to another server in your name. They even use your own cookies to authenticate for protected actions. So, how do you prevent these requests to your application?

Use GET and POST Requests Appropriately

The HTTP specification suggests that GET and HEAD requests should be used for “safe” actions only. Meaning, any web address you can type into a browser address bar should be used for retrieval only. Requests that perform actions or modify data should be made using one of the other request methods (POST, PUT, DELETE) and some sort of HTML form. This is something the Rails team has been pushing really hard with Rails 1.2 and beyond. Rails does provide workarounds with the _method parameter for user agents that don’t support PUT/DELETE, but they only work for POST requests. Your GET and HEAD requests should always be safe though.

The Secret Token Parameter

However, just requiring POST for unsafe actions isn’t enough. A compromised page can easily make scripted POST requests using XmlHttprequest objects or dynamically generated forms. So, the next thing I implemented was a secret token that is added as a hidden field on every generated form. I used a simple method that the Rails form tags call to automatically add the _method parameter, and changed it for my own purposes by added a _token parameter. This unique token key is generated by a combination of the user’s session id and some pre-determined server secret. The idea is that a compromised page may be able to get your session id, but would have no way of generating the correct token without knowing the server secret.

Enter: the CSRF Killer plugin

I created the CSRF Killer plugin (edge rails only) using these techniques. First, I had to worry about generating a unique token for every session.

# Generates a unique digest using the session_id and the CSRF secret.
def token_from_session_id
  key    = verify_token_options[:secret].respond_to?(:call) ? verify_token_options[:secret].call(@session) : verify_token_options[:secret]
  digest = verify_token_options[:digest] || 'SHA1'
  OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(digest), key, session.session_id)
end

# No secret was given, so assume this is a cookie session store.
def token_from_cookie_session
  session[:csrf_id] ||= CGI::Session.generate_unique_id
  session.dbman.generate_digest(session[:csrf_id])
end

You can then access the unique token using the #form_token helper in your views. However, I didn’t feel like updating all my forms and ajax actions, so I used some internal helper methods to try and insert the token where possible.

# Adds the _token to the :with option of #options_for_ajax unless :with is already used.
def options_for_ajax_with_security(options)
  options[:with] = "'_token=' + encodeURIComponent('#{escape_javascript form_token}')" unless options[:with]
  options_for_ajax_without_security(options)
end

# adds the token hidden input tag for all forms
def extra_tags_for_form_with_security(html_options)
  returning extra_tags_for_form_without_security(html_options) do |tags|
    tags << token_tag unless html_options['method'].to_s =~ /^get$/i
  end
end

Finally, we want to make sure that the unsafe rails actions verify the token properly.

def verified_request?
  !verifiable_request_format? || (request.method == :get || form_token == params[:_token])
end

def verifiable_request_format?
  request.format.html? || request.format.js?
end

Notice that the plugin is broken up into small methods where possible, allowing you to tweak minute details for your application. If SHA1 isn’t good enough, you’re free to override one of these methods and use something stronger.

The #verifiable_request_format? method above brings up another point. If all POST/PUT/DELETE actions require a token, what happens to those nice restful APIs you’ve been building? Nothing. Since I’m checking for html and js formats, I’m verifying that only normal HTTP and Ajax (with a text/javascript Content-Type) requests require a token. XML/Atom/JSON APIs typically won’t create sessions or use cookies, but will rather use HTTP Basic Authentication (or Digest, WSSE, etc). This protects those formats by default from CSRF attacks (unless your user/password has been compromised, and then you’re in trouble). Be sure to modify this method to add any custom formats that do use cookies for authentication, however.

All of this is available in my CSRF Killer if you want to play around with it. I’ve been using it in Lighthouse since its inception, and have been fixing it as issues arise. I’d like to see it stabilize so that we can have a sensible answer to the CSRF problem by default for all Rails applications.

Discussion

  1. Joe Joe said on March 6th

    Can you go into what you mean by:

    However, just requiring POST for unsafe actions isn’t enough. A compromised page can easily make scripted POST requests using XmlHttprequest objects or dynamically generated forms.

  2. Tim Lucas Tim Lucas said on March 6th

    Joe: did you check out the wikipedia article he linked to in the first paragraph?

    Pre v0.13 days I wondered why nobody cared about this… I even made the security extensions plugin, which got a little use but not widespread, and it’s now no longer being maintained. I’ve simply ignored the vulnerability as I haven’t been working on apps recently for which I felt it was justified, though that’s probably a bad excuse.

    The only thing I found I also needed was a few testing helpers to 1) perform requests with the secure token, and 2) assert the token is being used on the pages.

  3. rick rick said on March 6th

    Joe: AJAX requests by definition are dynamically generated using the XmlHttprequest object. You can also use the DOM to create a form object and submit it also. Rails does this with the link_to method if you pass a :method that’s not :get. ASP.Net also does something similar with its LinkButton server control.

    Tim: I suppose people just focus on the more immediate threat of XSS attacks. I don’t think CSRF attacks are a huge deal for most apps unless they’re widespread and contain valuable personal info. Gmail fits this perfectly. Your standard little web app (Lighthouse included!) probably doesn’t. I’ve been hacking on this plugin merely as an academic exercise for now.

  4. Emin Emin said on March 6th

    Thanks, Tim, very useful!

  5. Chris Anderson Chris Anderson said on March 6th

    One nice side effect of this approach is that it makes for built-in decent comment spam protection. If you use the secret _token even on unauthenticated forms (like for blog comments) most spam bots will not bother to fetch the page before attempting a post, and so will have an outdated token. I’ve been using this approach on my blog for a while, and I haven’t seen spam since I started.

  6. qtux qtux said on March 30th

    If you have ajax post requests on a page which should able to call serval times, for example a comment form. So you need to renew the token on the client side, so you have to send the new token via the ajax response. Now you are able to call the ajax function as much as you like if analyze the response. How can I prevent this?

  7. Mike Owens Mike Owens said on April 2nd

    It’d be nice if CsrfKiller had a parameter for enforcing the token on some GET requests. The GMail CSRF flaw a while back was just an information leak on a properly used (retrieval only) GET request.

    The easiest solution to this problem is exactly the same as protecting destructive/mutating actions.

    As a side note, it’d be really slick for CsrfKiller to check request.env[‘HTTP_X_TOKEN’] or something, at least when request.xhr? so you could move the token handling a little more out-of-band from RESTful URLs.

    CsrfKiller is awesome, thanks for saving me some time by providing it.

  8. rick rick said on April 2nd

    Mike: thanks for the suggestion, patches are welcome though :) It shouldn’t be tough to require the token on some GET requests… Perhaps I do that only for non HTML requests? Course, I’ll need some way to snag a token through an API call too.

Sorry, comments are closed for this article.