Reimplementing Rails’ CSRF protection in Sinatra

To hone my intuitions about Cross Site Request Forgery protection, I’ve been reimplementing Rails’ defence against t̶h̶e̶ ̶d̶a̶r̶k̶ ̶a̶r̶t̶s̶ CSRF in a Sinatra app I’ve been working on. If I can build something analogous to the real thing, that should help me see any blind spots more easily than just reading about Rails’ implementation, which happens ‘magically’ behind the scenes.

CSRF?

CSRF. If your app has an endpoint that’s listening for form submissions, then by default it can receive a request from anywhere and anybody. So we want to be sure that any form submitted has come from our own app: it’s authentic.

My app

In the Before Epoch, when pensioners frolicked in the streets and strangers breathed down each others’ necks on the tube, I used to love a word game called Snatch. I’ve been trying to recreate the glory of this game online, here.

A screenshot of my online version of the game Snatch
A screenshot of my online version of the game Snatch
A screenshot of my work-in-progress app
ws.send(JSON.stringify({
action: 'word',
room: '2',
handle: 'my arch nemesis',
word: 'floccinaucinihilipification' }));
module Snatch
class App < Sinatra::Base
enable :sessions
use Snatch::SnatchBackend
# ...
end
end
get '/' do
create_authenticity_token
erb :"index.html"
end

private

def create_authenticity_token
@authenticity_token = rand().to_s
session[:authenticity_token] = @authenticity_token
end
ws.send(JSON.stringify({
authenticity_token: '0.123456789',
action: 'word',
room: '2',
handle: 'my arch nemesis',
word: 'floccinaucinihilipification' }));
module Snatch
class SnatchBackend
...



def call(env)
@session = Rack::Request.new(env).session

if Faye::WebSocket.websocket?(env)
ws = Faye::WebSocket.new(env, nil, { ping: KEEPALIVE_TIME })

...

ws.on :message do |event|
@data = JSON.parse(event.data).transform_values { |value| ERB::Util.html_escape(value) }

if authenticated?
initialize_room if @redis.hget(room_key, 'tiles').nil?
perform_any_actions

@redis.publish(CHANNEL, JSON.generate(data_to_publish))
end
end
...
end
end

private

def authenticated?
@session['authenticity_token'] == data['authenticity_token']
end

...
end
end

It’s not exactly Rails

Among various differences between this hacked-together implementation and Rails, are:

  • I’m not resending the session cookie with each request (I think?), because I’m not using HTTP. So the session data used in SnatchBackend is probably unchanged since the most recent page load.

Questions to investigate further

Why does Rails store the authenticity token in the session cookie? Isn’t it easier to steal from there than from the session itself (on the server), since the session cookie lives on the user’s own computer (in the browser)? Perhaps this is related to the fiddly cryptography stuff. But if there is a way to calculate a ‘masked’ token (for the request) from the stolen cookie’s token, it seems eminently possible to ‘authenticate’ oneself using that stolen session cookie. Or maybe it’s just that there is no significant difference in difficulty in stealing something in the session cookie vs in the session per se (as the cookie unlocks the session).

This is my programming blog. www.github.com/david-mears