The Invalid Authenticity Token Experiment
I like experimenting. My most recent experiment was to make this site use SSL throughout. Given that it is currently just a blog, https isn’t strictly necessary. But that isn’t the point. I had already tried Amazon’s Route 53 for DNS and now I wanted to play with something else.
I decided to give Cloudflare a shot. I heard
about it from a friend and wanted to see what it was all about. As one might
expect, the new technology created a few bumps in the road. Most notably, form
posts were taunting me with 422 - Unprocessable Entity
errors. Here is how I
solved it.
Ingredients
- 64bit Amazon Linux 2017.03 v2.4.2 running Ruby 2.3 (Puma)
- Nginx 1.10.3
- Rails 5.1.2
- Cloudflare with flexible SSL
Problem
The switch to Cloudflare was quick and easy. So easy in fact, that I couldn’t resist turning on SSL everywhere while I was there. It was a simple switch. I didn’t have to request a certificate or do anything complicated at all. Nice.
The site worked just as it had before except now I had a nice green padlock in my browser. Life was good . . . until I tried to post the next article.
I use AJAX form submissions (via the Rails remote
option) and for the first
time, the POST request was answered with a 422 - Unprocessable Entity
error.
The Rails production log had this to say:
HTTP Origin header (https://ramekintech.com) didn't match request.base_url (http://ramekintech.com)
Completed 422 Unprocessable Entity in 1ms
ActionController::InvalidAuthenticityToken
The difference was the addition of SSL. A simple letter ‘s’ in the origin header broke form submission on my site. Bummer.
Solution
The solution was all about the Nginx configuration. I learned from this rails
issue that there were a few
additional headers that needed to be put into my nginx.conf
file. Until now,
I had been using the configuration that came with the default AWS Elastic
Beanstalk installation. Here are the changes I had to make:
http {
...
upstream myapp {
server unix:/var/run/puma/my_app.sock;
}
...
server {
...
server_name ramekintech.com;
...
location / {
proxy_pass http://myapp;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-Host $host;
}
...
}
...
}
Items to note:
- The location and name of the puma socket file may vary for other installations.
myapp
andmy_app
are not placeholders. Everything shown is precisely what solved the problem for me.- The
server_name
was set tolocalhost
and that did not work. Changing it toramekintech.com
finally got it all working.
The Other Solution
The other solution I found to this problem was to turn off
CSRF
(Cross Site Request Forgery) protection or disable it for routes that it caused
a problem for. I do not like that answer at all. I believe Rails gives us
this protection by default for a reason. If this is new to you, it is what the
protect_from_forgery with: :exception
line in the ApplicationController
is
doing.
I’ve read that the attack isn’t very prevalent. The Rails Security Docs state that “CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures)”. But that doesn’t matter to me. I want to be as secure as possible. Especially when usability is not compromised.
Final Bits
If CSRF protection ever gives you grief, please don’t just disable it for a quick fix. Find out why it is happening and solve the real problem. Every little bit helps to make the web a safer place for everyone. It took some extra effort to get my site working again but I feel it is time well spent.
Here is the full nginx.conf
file just in case there are other settings that
contributed to my success. The SSL section is not omitted. Because Cloudflare
is handling the https traffic, I do not have or need an SSL section here. I
know this creates a section in the request chain that is not encrypted. But,
like I said, I am experimenting. I’ll be solving that problem with the next
experiment.
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
index index.html index.htm;
upstream myapp {
server unix:/var/run/puma/my_app.sock;
}
server {
listen 80 ;
listen [::]:80 ;
server_name ramekintech.com;
root /usr/share/nginx/html;
include /etc/nginx/default.d/*.conf;
location / {
proxy_pass http://myapp;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-Host $host;
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
}