Auditing Your Rails Codebase for Malicious X-Forwarded-Host Headers
A practical guide to identifying, understanding, and mitigating Host header injection vulnerabilities in Ruby on Rails applications.
The Problem with Return Addresses
In the physical postal system, the return address on an envelope serves a specific purpose: it tells the recipient where to send a reply. However, that address is ultimately ink on paper. The postal service does not verify that the sender actually resides at the return address. If someone wishes to deceive a recipient, they can write any address they choose in the top left corner of the envelope.
This same principle applies to modern web architecture. When an HTTP request reaches your Ruby on Rails application, it carries various headers — including the Host and X-Forwarded-Host headers. These headers act much like a return address, telling your application where the request supposedly originated and where to direct subsequent responses.
Strictly speaking, these headers are entirely text provided by the client. Unless you have specific safeguards in place, an attacker can write any address they choose into these headers. This leads to a class of vulnerability known as Host header injection. Before we get into how to audit and fix this in your codebase, though, we should understand exactly how this vulnerability manifests in a Rails environment.
Understanding X-Forwarded-Host Injection
To understand the vulnerability, we need to examine how modern web requests are routed. Most Rails applications do not sit directly on the public internet. Instead, they operate behind a reverse proxy or load balancer — such as Nginx, HAProxy, or an AWS Application Load Balancer.
When a client makes a request to https://www.yoursite.com, the proxy receives the request and forwards it to your Rails application. Because the proxy initiates the connection to Rails, the application sees the proxy’s IP address and internal hostname. To preserve the original client information, proxies use headers like X-Forwarded-For and X-Forwarded-Host.
The vulnerability occurs when a Rails application blindly trusts the X-Forwarded-Host header and uses it to construct fully qualified URLs.
For example, consider a typical password reset flow. A user requests a password reset, and the application generates an email containing a link with a secure token. If the application uses the incoming request’s host to build that URL, an attacker can manipulate the process.
An attacker might send a request like this:
POST /users/password HTTP/1.1
Host: www.yoursite.com
X-Forwarded-Host: evil-attacker-domain.com
Content-Type: application/x-www-form-urlencoded
user[email]=victim@example.com
If the application trusts the X-Forwarded-Host header, it will generate a password reset link pointing to https://evil-attacker-domain.com/users/password/edit?reset_password_token=123. The victim receives a legitimate-looking email from your application, clicks the link, and unwittingly hands their reset token to the attacker.
Auditing Your Codebase
To determine if your application is vulnerable, you must audit how your codebase handles URL generation and redirects. We need to identify any location where the application uses the request’s host to build a URL dynamically.
You can start by searching your codebase for common methods that access the request host.
$ grep -r "request.host" app/
$ grep -r "request.host_with_port" app/
$ grep -r "request.base_url" app/
When reviewing the output, you should look for patterns where these values are passed into mailers, background jobs, or redirect paths.
Of course, finding explicit calls to request.host is only part of the battle. Rails provides many helper methods that implicitly use the request host when generating URLs. You should also audit your Action Mailer configurations and controller redirects.
If your mailers use the _url helpers (like reset_password_url) without a explicitly configured default_url_options[:host], they may fall back to using the host from the current request thread, depending on your Rails version and configuration.
Mitigating the Risk
There are three major approaches to mitigating Host header injection vulnerabilities. Depending on your infrastructure and Rails version, one of them may be more appropriate for your environment.
Option 1: Rails Host Authorization
The first option is utilizing the Host Authorization middleware built into Rails. Introduced in Rails 6, this middleware explicitly defines which hosts the application is allowed to serve. This is my preferred method, as it stops malicious requests at the framework level before they reach your application logic.
You can configure this in your environment files (e.g., config/environments/production.rb):
# config/environments/production.rb
Rails.application.configure do
# ...snip...
config.hosts << "www.yoursite.com"
config.hosts << "yoursite.com"
# ...snip...
end
When a request arrives with a Host or X-Forwarded-Host header that does not match the allowed list, Rails will reject it with a 403 Forbidden response.
One may wonder: what if we have dynamic subdomains, like a multi-tenant SaaS application? The answer is straightforward. The config.hosts setting accepts regular expressions, allowing you to authorize entire wildcard domains:
config.hosts << /.*\.yoursite\.com/
Option 2: Proxy-Level Header Stripping
The second approach is to handle the headers at your infrastructure layer. If your application relies on a reverse proxy, you can configure the proxy to strip or explicitly set the X-Forwarded-Host header, ignoring any value provided by the client.
For instance, in an Nginx configuration, you might explicitly set the header to the server’s known name:
proxy_set_header X-Forwarded-Host $server_name;
This approach, though, requires careful coordination between your infrastructure and application code. If your application is ever deployed in an environment without this proxy configuration, the vulnerability will return.
Option 3: Hardcoding Mailer Hosts
The third option is to ensure that critical components — particularly Action Mailer — never rely on the incoming request to determine the host. You should explicitly define the host in your environment configurations.
# config/environments/production.rb
Rails.application.configure do
config.action_mailer.default_url_options = { host: 'www.yoursite.com', protocol: 'https' }
end
By setting this explicitly, any _url helpers used in your email templates will use the configured host, regardless of what headers the client provided. Generally speaking, you should implement this configuration even if you use the Host Authorization middleware, as it provides an additional layer of defense.
Verifying the Fix
After implementing these mitigations, you should verify that your application correctly handles malicious headers. We can test this using curl from the command line:
$ curl -I -H "X-Forwarded-Host: evil.com" https://www.yoursite.com/
If you implemented the Rails Host Authorization middleware, you should expect to see a 403 Forbidden response:
HTTP/2 403
Content-Type: text/html; charset=UTF-8
This confirms that the framework is actively inspecting and rejecting unapproved hosts.
Maintaining Security Posture
Addressing X-Forwarded-Host vulnerabilities requires understanding the boundary between your application and the public internet. By explicitly defining which hosts your application trusts, and by decoupling critical processes like email generation from client-provided data, you significantly reduce your attack surface.
As web architectures grow more complex, with multiple layers of proxies and load balancers, we must be increasingly careful about which headers we trust. By implementing these safeguards, you ensure that your application routing remains secure and predictable.
Sponsored by Durable Programming
Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.
Hire Durable Programming