rails: Request origin does not match request base_url

I am running the Rails 5.0.0 beta1 and after deploying to production have noticed something odd. My application is running being an Nginx reverse proxy which decrypts SSL.

Every time I try to submit a form, a ActionController::InvalidAuthenticityToken is raised. I have managed to narrow this down to line 399 in ActionController. The problem is that the request origin has https:// as the protocol and the request base_url has http:// as the protocol.

I have tried setting the X-Forwarded-Proto header and using config.force_ssl = true but neither made any difference.

I have not yet figured out a way to isolate a failing test case but am happy to look into it if anyone has any suggestions.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 45
  • Comments: 33 (3 by maintainers)

Commits related to this issue

Most upvoted comments

I also ran into this issue. I’ll describe it differently to help with future searches.

Submission of any form to Nginx proxying to a Puma unix socket running a Rails 5.0.0.beta1 app responds with:

Can't verify CSRF token authenticity

and throws:

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken)

even though the App’s CSRF is set up correctly. The app works in development (puma or thin, without Nginx), and worked in Nginx/Puma/Rails-4.2. I don’t know if this is specific to using Puma, but seems like the same problem could exist with other servers (unicorn, passenger, etc.)

I don’t know if this behavior change is an error or intended.

I can confirm @tpbowden’s solution worked for me as well. In my nginx configuration, I setup the app to proxy to puma listening on a unix socket. Here are the relevant parts of the configuration.

upstream myapp {
  server              unix:///path/to/puma.sock;
}
...
location / {
  proxy_pass        http://myapp;
  proxy_set_header  Host $host;
  proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header  X-Forwarded-Proto $scheme;
  proxy_set_header  X-Forwarded-Ssl on; # Optional
  proxy_set_header  X-Forwarded-Port $server_port;
  proxy_set_header  X-Forwarded-Host $host;
}

My problem was similar, using heroku and cloudflare, solution was:

Make sure you have working SSL and HTTPS on Heroku (or wherever you’re serving your Rails application.) Turn Cloudflare SSL to Full mode. Problem solved.

from: http://til.obiefernandez.com/posts/875a2a69af-cloudflare-flexible-ssl-mode-breaks-rails-5-csrf

I’ve just done a bit more research and it is definitely the intended behaviour. It can be disabled in your app config using config.action_controller.forgery_protection_origin_check = false. It is enabled by default however.

Thanks so much : I was missing this line : proxy_set_header X-Forwarded-Proto $scheme;

It might not be related but just for your information.

I recently encountered the same issue. For those who do not use Nginx reverse proxy, you can create a middleware to rewrite X-Forwarded-Proto based on Cloudflare CF-VISITOR header, here is my solution:

# frozen_string_literal: true

require 'json'

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

  def call(env)
    return @app.call(env) unless env['HTTP_CF_VISITOR']

    env['HTTP_X_FORWARDED_PROTO'] = JSON.parse(env['HTTP_CF_VISITOR'])['scheme']
    @app.call(env)
  end
end

In config/application.rb:

config.middleware.use CloudflareProxy

You can also rewrite X-Forwarded-Port and X-Forwarded-Ssl and X-Forwarded-Host by the same technique. In my case, rewrote only X-Forwarded-Proto solved my problem.

A reference for CF-Visitor header:

https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-

CF-Visitor

A JSON object containing only one key called scheme. The value is identical to that of X-Forwarded-Proto (either HTTP or HTTPS). CF-Visitor is only relevant if using Flexible SSL.

The issue isn’t related to Puma and would happen to any web server running behind a reverse proxy which isn’t passing the correct headers through. The way I managed to figure out how this issue was being caused was by following the error down the stack trace.

The root of the issue is the code in request.origin == request.base_url, which compares the Origin header’s value against the base_url which is build up by a Rack::Response. There are 3 parts to this value, the scheme, the host and the port. All three of these are calculated based on a series of headers which not present by default when using a reverse proxy.

You can follow the source for Rack::Request#base_url here and it will become clear by looking in your web logs which headers are missing.

Fix by adding more headers in Nginx (X-Forwarded-Ssl on, X-Forwarded-Port 443 and X-Forwarded-Host hostname)

For those using Apache, this worked for me:

RequestHeader set X-Forwarded-Proto "https"

@afair You, Sir, have saved a lot of my time. Thank you.

If you look at @afair’s comment above he was referenced a correct Nginx config, you just need to add the x-forwarded headers related to ssl. I figured out which ones were missing by digging though the source code of Rack’s request object to find out how it determine’s the protocol of the request.

@Onumis Thank so much! I turn Cloudflare SSL to Full mode and fixed the problem.

This config helped me:

  location @app {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-SSL-Client-Cert $ssl_client_cert;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_redirect  off;
  }

Thanks @tpbowden and @afair I was desperatly trying to make any sense of this oddly failing CSRF protection… then I found this issue 👍

ahh, so how did you find out what was missing…and how did you add the missing headers? Thanks so much for the quick reply!