Security advice
CSRF protection
Faye lets you write server-side extensions that have access to the request
data for the current incoming message. This allows you to use the Cookie
header to authorize messages based on the user’s session, but you must
implement CSRF protection.
CSRF stands for ‘cross-site request forgery’, and it works like this. When a
web browser makes a request, it searches for any cookies it has stored that
are scoped to that URL. Cookie scope is determined by various factors,
including the domain and path of the URL, and whether it uses https
. It
doesn’t matter which page initiates the request, or what kind of request it
is; the browser attaches all in-scope cookies to every request and sends
them in the request’s Cookie
header.
Since Faye allows cross-origin connections, any site the user has open can
send requests to your Faye server, and they will have the user’s session
cookies attached. This means you can’t use Cookie
on its own to
authenticate a message since you don’t know where the message came from. You
must prove the message actually came from your site.
You can’t use the Origin
header for this, since the browser doesn’t send
an Origin
header for most transports that Faye uses. So, you need
implement explicit CSRF protection.
Most web frameworks do this as follows:
- Generate a large random token and store it in the user’s session
- Add this token to all forms using an
<input type="hidden">
- Reject all
POST
requests that don’t have a form parameter matching the session’s token
The idea is that other sites can’t scrape the token out of your site’s pages since sites can’t access each other’s DOM. Since each session gets a different token, an attacker site would need to steal the user’s cookies for your site, or access your site’s DOM, both of which the browser prevents.
The best way to implement CSRF protection is to piggy-back on the protection provided by your web framework. The following examples cover some common use cases.
Example: Rails
Rails includes CSRF protection that uses a session variable called
_csrf_token
. You can check incoming messages and compare their attached
token against the value stored in the session. If the tokens do not match,
you add an error to make the server reject them. You delete csrfToken
from
the message so that it’s not forwarded to subscribers.
class CsrfProtection < ActionController::Base def incoming(message, request, callback) message_token = message['ext'] && message['ext'].delete('csrfToken') unless valid_authenticity_token?(request.session, message_token) message['error'] = '401::Access denied' end callback.call(message) end end
This extension needs to access the Rails session, and to allow this you must
put Faye at a specific place in the Rails middleware stack. In your
application config, add Faye::RackAdapter
after your app’s session store
middleware, adding CsrfProtection
as an extension.
config.middleware.insert_after ActionDispatch::Session::CookieStore, Faye::RackAdapter, :extensions => [CsrfProtection.new], :mount => '/bayeux', :timeout => 25
Finally you need to make the client attach the CSRF token to all messages it
sends. Rails includes the token in all pages using a meta
tag called
csrf-token
, and you can use jQuery to grab its content and attach it to
outgoing messages.
var client = new Faye.Client('/bayeux'); client.addExtension({ outgoing: function(message, callback) { message.ext = message.ext || {}; message.ext.csrfToken = $('meta[name=csrf-token]').attr('content'); callback(message); } });
Example: Sinatra and other Rack apps
If you’re using Sinatra or any other Rack-based framework, you can use standard Rack middlewares to add session support and CSRF protection to your stack.
Start by making a Faye extension that blocks incoming messages that lack a token matching the CSRF token stored in the session.
class CsrfProtection def incoming(message, request, callback) session_token = request.session['csrf'] message_token = message['ext'] && message['ext'].delete('csrfToken') unless session_token == message_token message['error'] = '401::Access denied' end callback.call(message) end end
To enable session support, you need a Rack session middleware before Faye
in your config.ru
middleware stack.
Here’s a sample config.ru
for this design:
require 'faye' require 'rack' require './path/to/sinatra_app' use Rack::Session::Cookie, :secret => '55912e33cad379c8ff64c8a97fe2f096ef013111' use Faye::RackAdapter, :extensions => [CsrfProtection.new], :mount => '/bayeux', :timeout => 25 run Sinatra::Application
You’ll need to generate and render the CSRF token in your templates for the client to pick up:
<% token = request.session['csrf'] ||= SecureRandom.base64(32) %> <meta name="csrf-token" content="<%= token %>">
And finally, configure the client to attach this token to outgoing messages.
var client = new Faye.Client('/bayeux'); client.addExtension({ outgoing: function(message, callback) { message.ext = message.ext || {}; message.ext.csrfToken = $('meta[name=csrf-token]').attr('content'); callback(message); } });
Example: Express
Express uses Connect middlewares to provide session support and CSRF protection. With a little work, you can use these middlewares with Faye to include CSRF protection in your extensions.
Start by adding the cookieParser
, cookieSession
and csrf
middlewares
to your stack. Here’s a quick example with an app with one request handler.
We keep a reference to the cookieParser
and cookieSession
middlewares
since we’ll need to use them to process Faye requests.
var express = require('express'), ejs = require('ejs'), app = express(), parser = express.cookieParser(), session = express.cookieSession({secret: '55912e33cad379c8ff64c8a97fe2f096ef013111'}); app.use(parser); app.use(session); app.use(express.csrf()); app.engine('html', ejs.renderFile); app.get('/', function(request, response) { response.render('index.html', {request: request}); }); var server = app.listen(8000);
Next, attach the Faye server to your app.
var faye = require('faye'); var bayeux = new faye.NodeAdapter({mount: '/bayeux', timeout: 45}); bayeux.attach(server);
Now you can add an extension that checks whether the incoming message has a CSRF token that matches the one in the session. Since this requires session access but Faye is outside the Connect middleware stack, you need to use the middleware you created earlier to pre-process the request before your extension handles it. You can do this with a helper function:
var EventEmitter = require('events').EventEmitter; var buildSession = function(request, callback) { var response = new EventEmitter(); parser(request, response, function() { session(request, response, callback); }); };
Now you can add the CSRF protection extension. Remember to delete
csrfToken
from the message so it’s not forwarded to any subscribers.
bayeux.addExtension({ incoming: function(message, request, callback) { buildSession(request, function() { var sessionToken = request.session._csrfSecret, messageToken = message.ext && message.ext.csrfToken; if (message.ext) delete message.ext.csrfToken; if (sessionToken !== messageToken) message.error = '401::Access denied'; callback(message); }); } });
Finally, you need to render the CSRF token in your templates:
<meta name="csrf-token" content="<%= request.session._csrfSecret %>">
And use jQuery to pick that token up and attach it to outgoing messages from the client:
var client = new Faye.Client('/bayeux'); client.addExtension({ outgoing: function(message, callback) { message.ext = message.ext || {}; message.ext.csrfToken = $('meta[name=csrf-token]').attr('content'); callback(message); } });