Trying to use Rails CSRF protection on cached actions? Rack middleware to the rescue!

With the release of Rails 2.0 came some very nice security enhancements. Among those was CSRF protection (cross site request forgery) which is implemented by putting a server side generated token into a hidden field inside of forms with POST, PUT, or DELETE as the action/method. When Rails is asked to render a page that contains a form with any of these methods it generates an authenticity token. This token is stored in the session object and rendered out to the previously mentioned hidden field. Anytime Rails receives a POST, PUT or DELETE request to one of these actions it will ensure that it was given back the previously generated token or else raise an error and not execute the action.

Now let’s consider action caching: The very first user that comes to the page (assuming the cache is currently clear) will get initiate a full dive through the rails stack will get the correct CSRF token both in the rendered form and in his/her session object on the server. The next person that comes along and hits the same page will receive the FIRST person’s CSRF token (incorrectly) and will not have any token at all existing in their session object. If this second user, who was served up an action cached page, tries to submit said form they will experience failure because they will be (incorrectly) sending back the token intended for the first user.

In some cases you can easily get around this by disabling CSRF protection on an action by action basis, but this leaves that action vulnerable.

A work around is to add the CSRF token to all protected forms AFTER Rails returns the cached response. We can do this with a Rack middleware application. The basic strategy is to override the default behavior of the token generation method in the rails form helps and have it spit out a string that we can later match against. We can accomplish this by adding a little monkey patch custom initializer in config/initializers/form_tag_helper.rb

  module ActionView
    module Helpers
      module FormTagHelper
        alias_method :token_tag_rails, :token_tag

        # Make all forms generate the same forgery_protection_token so that
        # they can be replaced by Rack before being sent back to the user.
        def token_tag
          if protect_against_forgery?
            tag(
              :input, :type => "hidden",
              :name => request_forgery_protection_token.to_s,
              :value => "__CROSS_SITE_REQUEST_FORGERY_PROTECTION_TOKEN__"
            )
          else
            ''
          end
        end
      end
    end
  end

Normally this method is responsible for generating a hidden field with the CSRF token as the value. Since we want this to work with action cached pages we need to have this method return a string that is both predictable and relatively unique. More on this in a moment… let’s take a look at the middleware itself.

I put this file in lib/middleware and then added that directory to my load path in environment.rb

  class CachingWithRequestForgeryProtection
    def initialize(app)
      @app = app
    end

    def call(env)
      status, headers, response = @app.call(env)

      if response.is_a? ActionController::Response
        response.body = response.body.gsub(
          "__CROSS_SITE_REQUEST_FORGERY_PROTECTION_TOKEN__",
          response.instance_variable_get(:@session)[:_csrf_token]
        )
        headers["Content-Length"] = response.body.length.to_s
      end

      [status, headers, response]
    end
  end

Rack middleware allows you to directly manipulate Rails request and Response objects outside of the Rails app itself. In this case the middleware app will be looking for the string “CROSS_SITE_REQUEST_FORGERY_PROTECTION_TOKEN” and will replace it with the real token. We get this by directly accessing the users sessions data (the place where the authenticity tokens are put after generation) from within our middleware.

The last few details necessary to get this thing operational are to: make sure Rails loads the new middleware…

  # in environment.rb
  config.load_paths += %W{#{RAILS_ROOT}/lib/middleware}
  config.middleware.use "CachingWithRequestForgeryProtection"

and to make sure that an authenticity token is being generated for each user and put into their session data.

  # in application controller
  before_filter :form_authenticity_token

Because Rack middleware operates outside of the Rails caching layer we can send the correct user specific, unique authenticity token to each user without sacrificing action caching or CSRF protection. The overhead added by the string replacement is negligible and well worth the added security.

blog comments powered by Disqus