Home

Cross-Site Request Forgery Prevention in Sinatra

in CSRF, Sinatra, and Security

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
2
3
4
5
6
7
8
$(document).on 'submit', 'form', ->
  form   = $(this)
  method = form.attr('method').toUpperCase()
  token  = $('meta[name=csrf-token]').attr('content')

  # add the CSRF token as a hidden input to the form
  if method? and method isnt 'GET'
    form.prepend $('<input>', name: '_csrf', type: 'hidden', value: token)

The Sinatra App

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
before do
  session[:csrf] ||= SecureRandom.hex(32)

  response.set_cookie 'authenticity_token', {
    value: session[:csrf],
    expires: Time.now + (60 * 60 * 24 * 180), # that's 180 days
    path: '/',
    httponly: true
    # secure: true # if you have HTTPS (and you should) then enable this

  # this is a Rack method, that basically asks
  #   if we're doing anything other than GET
  if !request.safe?
    # check that the session is the same as the form
    #   parameter AND the cookie value
    if session[:csrf] == params['_csrf'] && session[:csrf] == request.cookies['authenticity_token']
      # everything is good.
    else
      halt 403, 'CSRF failed'
    end
  end
end