Rack::Cors Configuration Tricks

cyu’s Rack::Cors middleware is rather handy if want to control your CORS (Cross-Origin Resource Sharing) settings in a Ruby-on-Rails project. Previously, there was a fairly major issue where :credentials => true was the default (which you generally do not want), but there were also some more complicated tweaks that I wanted to make.

One problem I recently had to deal with was wanting to:

  • Allow CORS connections from arbitrary domains (this site functions as an API)
  • Do not allow CORS from http domains at all
  • Only allow cookies (Access-Control-Allow-Credentials) to be sent for sibling subdomains
  • Prevent cookies from being sent from specific sibling subdomains (that are actually run by a third party)
  • On development (non-production) versions of the site, allow credentials from localhost

In short, here is the rough configuration:

# The main domain, this might be different per environment
domain = 'example.com'

# These subdomains are not trusted and should not be allowed to authenticate with cookies
cors_third_party_subdomains = [
    'blog',
    'shop',
    'support'
].join('|')

# These are the Rack::Cors settings that we want to set, first for all domains and then for trusted ones
cors_headers = {
    :headers => :any,
    :methods => [:get, :post, :put, :delete, :options, :patch],
    :expose => %w(Link),
    :credentials => false
}
cors_headers_internal = cors_headers.merge({:credentials => true})

# This is the actual Rack::Cors configuration
config.middleware.insert_before 0, Rack::Cors do
    # Third party subdomains should not get cookies in case of XSS
    allow do
        origins /^https:\/\/(#{cors_third_party_subdomains})\.#{domain}$/
        resource '*', cors_headers
    end

    # We only want allow-credentials to be true for our own requests
    # Otherwise you'll need to supply or other non-cookie credentials
    allow do
    origins /^https:\/\/(|[^.]+\.)#{domain}$/
    resource '*', cors_headers_internal
    end

    # All other requests made over https allow CORS without credentials
    allow do
    origins /^https:\/\//
    resource '*', cors_headers
    end

    # Allow connections from localhost on non-prod environments as internal requests
    unless Rails.env.production?
        allow do
            origins /^(https?:\/\/)?localhost(:\d+)?$/
            resource '*', cors_headers_internal
        end
    end
end

I’m sure there are other ways to deal with this, but I spent a little whiel working on it and finally got just what I wanted.

A few gotchas to keep in mind:

  • You need to make sure to specify the entire origin to prevent suffix attacks (such as example.com.evil.com).
  • You also should specific the scheme to prevent downgrade attacks.
  • You should turn off credentials for any domain you don’t trust.

Give it a try.