It is common practice to run a Rails app using a custom domain locally. app.lvh.me
is very common. We also use ngrok.io
sometimes to interact with third party services. If you are using a custom domain on a Rails 6 app, you will see an error with the very first request itself when you hit app.lvh.me
in the browser.
But if we just try localhost:3000
then it works. We see this error only when using a custom domain.
DNS rebinding attack
This error is raised because Rails 6 has added protection against DNS rebinding attacks. But what is DNS rebinding attack in the context of a Rails app running locally?
It is a form of attack where a web page can cause malicious JavaScript code to run against local Rails app by changing the DNS address of the original server to localhost. The malicious script can compromise the system by executing random code against your Rails app running locally.
Web console
Rails ships with web-console in development environment to help debug errors while developing the app. web-console gem was vulnerable to the DNS rebinding attacks till Rails 6 as the attacker could steal the session id used by web-console and then make Ajax requests using this session id to execute arbitrary code.
An example of this attack is a script to open calculator app on your system while the Rails app is running locally.
Guard against DNS rebinding attack in Rails 6
In Rails 6, a new middleware HostAuthorization
is added which provides a guard against the DNS rebinding errors. The middleware is included in all environments but it gets kicked in development environment by default.
It works as follows:
- For a given incoming request, it checks the value of
config.hosts
. This configuration option is supposed to hold values of the hosts which are allowed by the Rails app. - If
config.hosts
is not empty, then the host of the incoming request is checked against all the allowed values byconfig.hosts
. This is done by comparing theHost
header as wellX-Forwarded-Host
header of the incoming request with the allowed hosts fromconfig.hosts
. - If there is at least one match then the request is authorized. But if there is no match then the the
Blocked host
error is thrown that we saw above.
Let's check what is the default value of this option.
irb(main):002:0> Rails.application.config.hosts
=> [#<IPAddr: IPv4:0.0.0.0/0.0.0.0>#<IPAddr:IPv6:0000:0000:0000:0000:0000:0000:0000:0000/0000:0000:0000:0000:0000:0000:0000:0000>,
".localhost"]
irb(main):003:0>
These values make sense as they are only allowing the requests from localhost
and 0.0.0.0
which are the most likely places from where a request will be made to the Rails app running locally.
So if we are using a custom domain like app.lvh.me
, we see the above error about host being blocked. The fix is simple, we need to allow lvh.me
in the config.hosts
We can fix it by adding following code in config/enviroments/development.rb
.
config.hosts << '.lvh.me'
By prefixing the host with
.
it allows all the subdomains of lvh.me as well. Soapi.lvh.me
will also be allowed by above code. It even allows Procs, regular expressions and IPAddr objects as values forconfig.hosts
.
What about production?
In production, the value for config.hosts
is not set by Rails. So no check is performed by the HostAuthorization
middleware. If you want to perform the header check then you will have manually add the allowed domain to the config.hosts
.
config.hosts << '.myawesomeapp.io`
Road to Rails 6
Want to know more about such small but important changes which are part of Rails 6 and which affect your day to day development? Subscribe to my newsletter and be on the course for making your app Rails 6 ready.
Top comments (0)