DNS rebinding attacks protection in Rails 6

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.

http://lvh.me is a free service that resolves itself along with all subdomains to localhost.

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 by config.hosts. This is done by comparing the Host header as well X-Forwarded-Host header of the incoming request with the allowed hosts from config.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:>#<IPAddr:IPv6:0000:0000:0000:0000:0000:0000:0000:0000/0000:0000:0000:0000:0000:0000:0000:0000>, 

These values make sense as they are only allowing the requests from localhost and 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. So api.lvh.me will also be allowed by above code. It even allows Procs, regular expressions and IPAddr objects as values for config.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`

But I want to disable this check, tell me how?

Just set config.hosts to nil in the config/application.rb file and this check will be disabled for all environments. You can also disable it conditionally for specific environment by adding this line toconfig/environments/<environment>.rb file.

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.