Sunday, 30 June 2013

The dreaded 'attack prevented by Rack::Protection::AuthenticityToken' problem

I have a Padrino app with multiple sub-apps.
Each of those sub-apps is not really an app, but rather a set of APIs.
To illustrate here is the super-basic layout:

├── API
│   └── v1
│       ├── airports
│       │   ├── app.rb
│       │   ├── config
│       │   │   └── boot.rb
│       │   ├── controllers
│       │   │   └── airports.rb
│       │   ├── db
│       │   │   └── seed.rake
│       │   └── models
│       │       └── airport.rb
│       └── sessions
│           ├── app.rb
│           ├── config
│           │   └── boot.rb
│           ├── controllers
│           │   └── sessions.rb
│           ├── db
│           │   └── seed.rake
│           └── models
├── Capfile
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
│   ├── app.rb
│   ├── controllers
│   ├── helpers
│   └── views
├── config
│   ├── apps.rb
│   ├── boot.rb
│   ├── database.rb
│   └── deploy.rb
├── config.ru
├── db
├── lib
├── library
├── log
├── models
├── public
│   ├── airports.html
│   ├── favicon.ico
│   ├── home.html
│   ├── images
│   │   └── logo.jpg
│   ├── javascripts
│   │   ├── application.js
│   │   ├── jquery-ujs.js
│   │   └── jquery.js
│   ├── login.html
│   ├── logout.html
│   └── stylesheets
│       ├── application.css
│       ├── bootstrap.css
│       └── reset.css
├── spec
│   ├── spec.rake
│   └── spec_helper.rb
└── tmp

Notice that folder 'API'? That's what holds all the sub-apps.
In this simple case, there are just two.
Each has a minimal set of folders and what not.
The bulk of the work is done in the public html files via jQuery.
So for example, you want a html file to have jQuery make a call to login a user:

The html:

<div id="ProfileLogin">
    <h1>LOGIN</h1>

    <div id="ProfileLoginErrors"></div>
    <form id="ProfileLoginForm" method="post">
        <label for="ProfileLoginUsername">Username:</label>
        <input id="ProfileLoginUsername" name="username" type="text"/>
        <label for="ProfileLoginPassword">Password:</label>
        <input id="ProfileLoginPassword" name="password" type="text"/>
        <input id="ProfileLoginSubmit" type="submit" value="Login"/>
    </form>
</div>

The jQuery:

    <script type="text/javascript">
        $(document).ready(function () {
            $('#ProfileLoginForm').on('submit', function (event) {
                event.preventDefault();
                var postData = $('#ProfileLoginForm').serialize()
                $.ajax({
                    type: 'POST',
                    url: '/api/v1/sessions/login',
                    data: postData,
                    success: function (data, textStatus, jqXHR) {
                        if (data.errors) {
                            $('#ProfileLoginErrors').html(data.errors);
                        } else {
                            window.location.href = '/home.html';
                        }
                    },
                    fail: function (data, textStatus, jqXHR) {
                        $('#ProfileLoginErrors').html(data);
                    }
                })
            });
        });
    </script>

Now when you do a POST, and look at the development log, you see this:

 WARN -  attack prevented by Rack::Protection::AuthenticityToken
DEBUG -      POST (0.0035s) /api/v1/sessions/login - 403 Forbidden

Damn. Rack::Protection::AuthenticityToken is stopping us getting in.
The problem is that Rack::Protection uses a hidden value to ensure you're being called from where it expects.

The code responsible is in /Users/[you]/.rvm/gems/ruby-2.0.0-p0/gems/rack-protection-1.5.0/lib/rack/protection/authenticity_token.rb.
It looks like this:

require 'rack/protection'

module Rack
  module Protection
    ##
    # Prevented attack::   CSRF
    # Supported browsers:: all
    # More infos::         http://en.wikipedia.org/wiki/Cross-site_request_forgery
    #
    # Only accepts unsafe HTTP requests if a given access token matches the token
    # included in the session.
    #
    # Compatible with Rails and rack-csrf.
    class AuthenticityToken < Base
      def accepts?(env)
        return true if safe? env
        session = session env
        token   = session[:csrf] ||= session['_csrf_token'] || random_string
        env['HTTP_X_CSRF_TOKEN'] == token or
          Request.new(env).params['authenticity_token'] == token
      end
    end
  end
end

And the reason is returns false is because 'authenticity_token' hasn't been provided.

Don't bother going into and mucking about with:

set :protection, true
set :protect_from_csrf, true
set :allow_disabled_csrf, true

It won't help. The problem is you're not passing the 'authenticity_token' in the POST request.
The cure:

  // REQUIRES the authenticity token appended!
  var postData = $('#ProfileLoginForm').serialize() + '&authenticity_token=' + CSRF_TOKEN;

Now where did that CSRF_TOKEN come from?
Ah. And here's the trick.
First modify your public/javascripts/applications.js and add this:

var CSRF_TOKEN = '';

function configureCSRF() {
    $.ajax({
        type: 'GET', url: '/api/v1/sessions/csrf_token',
        async: false,
        cache: false,
        success: function (data, textStatus, jqXHR) {
            CSRF_TOKEN = data.csrf;
        },
        fail: function (data, textStatus, jqXHR) {
        }
    })
}

Just see that a call is being made to '/api/v1/sessions/csrf_token' and on success, a global CSRF_TOKEN is being set.
So what is the call '/api/v1/sessions/csrf_token' look like?

get :csrf_token, :map => '/csrf_token', :provides => :json do
  logger.debug 'Retrieving csrf_token'
  result = {
      :csrf => session[:csrf]
  }
  JSON.pretty_generate result
end

Ok. So how do you use it?
You change your javascript to:

<script type="text/javascript">
    configureCSRF();
    $(document).ready(function () {
        $('#ProfileLoginForm').on('submit', function (event) {
            event.preventDefault();
            // REQUIRES the authenticity token appended!
            var postData = $('#ProfileLoginForm').serialize() + '&authenticity_token=' + CSRF_TOKEN;
            $.ajax({
                type: 'POST',
                url: '/api/v1/sessions/login',
                data: postData,
                success: function (data, textStatus, jqXHR) {
                    if (data.errors) {
                        $('#ProfileLoginErrors').html(data.errors);
                    } else {
                        window.location.href = '/home.html';
                    }
                },
                fail: function (data, textStatus, jqXHR) {
                    $('#ProfileLoginErrors').html(data);
                }
            })
        });
    });
</script>

You could also add:

headers: {
    'HTTP_X_CSRF_TOKEN': CSRF_TOKEN
},

To the call, but I found it was not necessary.

YMMV.

1 comment:

  1. Thanks! I also struggled with this, seems like you described my last day at work with this post.

    By the way, I noticed in rack-protection (1.5.3) the function accepts? checks for env['HTTP_X_CSRF_TOKEN'] before it checks for the env parameters, therefore setting the jquery XHR header of 'X_CSRF_TOKEN': 'your_token' would also work, instead of a parameter.

    In fact, something like this could work for most people (using Padrino-framework):

    $('body').bind('ajaxSend', function(elm, xhr, s){
    if (s.type == 'POST' || s.type == 'PUT' || s.type == 'DELETE') {
    xhr.setRequestHeader('X_CSRF_TOKEN', '#{csrf_token}');
    }
    });

    Cheers!

    ReplyDelete