Cross-Site Request Forgery Prevention in Sinatra

in CSRF , Security , Sinatra

Unlike Rails, Sinatra has no native cross-site request forgery prevention so I thought I’d take a minute to outline the implementation that I use to prevent these kinds of attacks. It’s pretty naive and simple, but that’s the kind of implementation I like.

If you’re unaware, cross-site request forgery (CSRF) is a very common vulnerability that exploits an existing session in a user’s browser and the inherent trust that establishes with the server to carry out malicious acts. Luckily, it’s fairly easy to prevent once you know about it.

A Pattern for Implementation

The implementation pattern I picked is called the “Synchronizer Token Pattern". Basically, we generate a token server-side that the client should send along with every request, thus (hopefully) ensuring trust between the client and server. The token needs to be really, truly random to avoid predictability and should be sent to the client in (at least) the HTML of the document. I send the token along as an HTTP-Only cookie as well to add another layer of extra-double-sureness to the prevention mechanism, but you don’t have to if you don’t want to.

Implementation

Code-wise, this is all in Sinatra and depends on having some session setup.

The Layout

1<meta name='csrf-token' content='<%= session[:csrf] %>'>

The JavaScript

Here, we hook into the submit event of any form and add the CSRF token to anything that isn’t a GET request:

1$(document).on 'submit', 'form', ->
2  form   = $(this)
3  method = form.attr('method').toUpperCase()
4  token  = $('meta[name=csrf-token]').attr('content')
5
6  # add the CSRF token as a hidden input to the form
7  if method? and method isnt 'GET'
8    form.prepend $('<input>', name: '_csrf', type: 'hidden', value: token)

The Sinatra App

 1before do
 2  session[:csrf] ||= SecureRandom.hex(32)
 3
 4  response.set_cookie 'authenticity_token', {
 5    value: session[:csrf],
 6    expires: Time.now + (60 * 60 * 24 * 180), # that's 180 days
 7    path: '/',
 8    httponly: true
 9    # secure: true # if you have HTTPS (and you should) then enable this
10
11  # this is a Rack method, that basically asks
12  #   if we're doing anything other than GET
13  if !request.safe?
14    # check that the session is the same as the form
15    #   parameter AND the cookie value
16    if session[:csrf] == params['_csrf'] && session[:csrf] == request.cookies['authenticity_token']
17      # everything is good.
18    else
19      halt 403, 'CSRF failed'
20    end
21  end
22end