sourcediver.org

about software and freediving

Rack-protection and Nginx

Nginx is currently a common choice when setting up reverse proxies for ruby web services. I currently serve all projects using unicorn, which creates a unix socket through which nginx talks to the application. The nginx configuration is pretty straightforward, you define a server with some rules and a location for your application. Whenever a location matches a path of your application, nginx will forward this request to the application through the unix socket.

In one particular project, I am using the padrino framework which uses sinatra’s rack protection to prevent common attacks like XSS or Cross-Site-Request-Forgery attemps.

The nginx config was something like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
upstream app_server {
  server unix:/var/sockets/padrino_app.sock fail_timeout=0;
}

server {
  listen         80;
  server_name    example.org
  return         301 https://$server_name$request_uri;
}

server {
  listen 443 ssl spdy;
  ssl on;
  ssl_session_cache shared:SSL:20m;
  ssl_session_timeout 10m;
  ssl_certificate /etc/ssl/private/example.org.crt;
  ssl_certificate_key /etc/ssl/private/example.org.key;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
  ssl_dhparam /etc/ssl/private/dhparam.pem;
  charset  utf-8;
  server_name  example.org;

  keepalive_timeout 5;
  root        /home/foo/example.org/current/public;
  access_log  /home/foo/example.org/shared/log/nginx_access.log;
  error_log   /home/foo/example.org/shared/log/nginx_error.log;
  rewrite_log on;

  location / {
    try_files $uri $uri/ /index.html =404;
  }

  location ~* ^/app/ {
    proxy_set_header Host               $host;
    proxy_set_header X-Forwarded-Host   $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Real-IP          $remote_addr;
    proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
    proxy_buffering  on;
    proxy_redirect   off;

    if (!-f $request_filename) {
      proxy_pass
      http://app_server;
      break;
    }
  }
}

This config always worked for setups without SSL so I was certain that it will just work with the SSL rewrite.

However I was getting 403 - Forbidden for all requests !GET. These errors also showed up in the log files:

attack reported by Rack::Protection::AuthenticityToken

I was puzzled since all requests had their proper authenticity token and nothing was different from my development configuration where everything worked, same ruby version, same gemset.

After some debugging I found the solution for the problem which is pretty simple and obvious.

rack-protection also checks the protocol which has been used for the request - if nginx does not send this information correctly, there will be a mismatch of the protocol (assumed http) and the referer (https) which will result in a 403.

Just add this line to your nginx config:

proxy_set_header X-Forwarded-Proto $scheme;

This will tell your application that the original request came in using https even though the request from nginx to unicorn is http.

Update 2015/12/02: The rack module that is responsible for the error seems to be Rack::Protection::HttpOrigin. Thanks to @907th for the hint!

Comments