The go-to resource for upgrading Ruby, Rails, and your dependencies.

Fixing CVE-2023-28362: Mitigating XSS via redirect_to in Action Pack


In 1999, the term “Cross-Site Scripting” was coined by Microsoft security engineers to describe a then-novel class of vulnerabilities where malicious scripts were injected into trusted websites. At the time, the concept was straightforward, but the implications were profound. Decades later, Cross-Site Scripting — often abbreviated as XSS — remains one of the most pervasive and dangerous threats in web application security.

Similarly, while Ruby on Rails provides robust defaults to prevent XSS, developers must remain vigilant. Security is an ongoing process, and vulnerabilities can appear in unexpected places — such as redirect URLs. This brings us to CVE-2023-28362, a potential XSS vulnerability in Action Pack related to the redirect_to method.

Before we get into that, though, let’s establish why this specific vulnerability matters and how it functions within a typical Rails application.

Understanding the Vulnerability

A fundamental feature of many web applications is redirecting a user after they complete an action. For example, when a user logs in, they are typically redirected back to the page they were originally trying to access.

You might see code like this in a Rails controller:


def create
  user = User.authenticate(params[:username], params[:password])
  if user
    session[:user_id] = user.id
    # Redirect the user to their original destination
    redirect_to params[:return_to] || root_path
  else
    render :new
  end
end

This code works well under normal circumstances. However, if an attacker controls the params[:return_to] value, they can manipulate the destination.

Rails, strictly speaking, explicitly checks for and blocks basic javascript: URIs in redirect_to calls. However, CVE-2023-28362 addresses a different flaw entirely: redirect_to allowed user-provided values to contain characters that are not legal in an HTTP header value.

When a Rails application returns an HTTP response with illegal characters in the Location header, downstream services — such as proxies, WAFs, or CDNs — might enforce RFC compliance by stripping the invalid Location header from the response. When the Location header is removed, the browser doesn’t automatically redirect. Instead, it renders the fallback HTML page that Rails generates for redirects. If the attacker’s payload included crafted characters designed to break out of the href attribute on that fallback page, they could inject a Cross-Site Scripting (XSS) payload that executes when the victim interacts with the page.

This is a classic reflection of a broader security principle: user input must never be trusted, even when it is used for seemingly benign operations like HTTP redirects.

Mitigating the Threat

There are two major approaches to mitigating this vulnerability within the Ruby ecosystem. Depending on the particular circumstances you find yourself in, one of them may be more useful than the other. Of course, we don’t need to discuss all possible network-level mitigations in this article; while you could conceivably configure a Web Application Firewall (WAF) to detect and block these specific illegal characters, addressing the issue within the application code or framework itself provides a more robust defense.

The Preferred Solution: Upgrading Rails

The first is to perform a Ruby and Rails upgrade; this is my preferred method. The maintainers of Ruby on Rails have released patched versions that correctly handle the protocol validation within Action Pack by escaping or rejecting invalid header characters.

You should upgrade to one of the following versions, or a newer release:

  • Rails 7.0.5.1
  • Rails 6.1.7.4

Note that this vulnerability was not officially patched in older, unmaintained versions such as 6.0 or 5.2. If you are on an older version, you must apply an application-level patch.

If you manage a large and complex application, planning a deliberate upgrade roadmap is often a reliable way to eliminate this and other known security risks. It addresses the root cause within the framework itself, rather than relying on application-level patches that might miss edge cases.

The Application-Level Workaround

The second is an application-level workaround. If a framework upgrade is not immediately feasible, you can explicitly validate the redirect URL before passing it to redirect_to.

Instead of passing the parameter directly, we can parse the URL and ensure it uses a safe scheme — or is a relative path. We can also explicitly reject any URLs that contain characters not permitted in HTTP headers.


def create
  user = User.authenticate(params[:username], params[:password])
  if user
    session[:user_id] = user.id
    redirect_to safe_redirect_url(params[:return_to])
  else
    render :new
  end
end

private

def safe_redirect_url(url)
  return root_path if url.blank?

  # Explicitly reject characters that are illegal in HTTP headers
  return root_path if url.to_s.match?(/[\x00-\x08\x0A-\x1F]/)

  parsed_url = URI.parse(url.to_s)
  
  # Allow relative paths or explicit HTTP/HTTPS schemes
  if parsed_url.relative? || %w[http https].include?(parsed_url.scheme.to_s.downcase)
    url
  else
    root_path
  end
rescue URI::InvalidURIError
  root_path
end

In this example, we first reject any string containing the illegal header characters that trigger CVE-2023-28362. Then, we parse the provided URL using Ruby’s standard URI library. We then strictly enforce that the scheme must be either HTTP or HTTPS, or that the URL must be a relative path. If the URL contains a malicious scheme like javascript: or data:, it will fall back to the root_path.

This explicit validation, though, is generally safer because it uses an allowlist approach rather than a blocklist approach. We define exactly what is permitted, rather than trying to anticipate every possible obfuscation an attacker might invent.

Auditing Your Codebase

Of course, finding and patching a single instance of a vulnerable redirect_to call is only part of the battle. Applications that have accumulated significant technical debt over years of development often contain multiple instances of unsafe redirects.

To systematically identify these issues, you can search your codebase for instances of redirect_to that accept parameters directly. You can use standard command line tools like grep to find potential hotspots:

$ git grep "redirect_to params"
app/controllers/sessions_controller.rb:    redirect_to params[:return_to] || root_path
app/controllers/oauth_callbacks_controller.rb:    redirect_to params[:state]
...snip...

As we can see, running the above search is a read-only process; it does not trigger any changes to your code, making it completely safe to run in any environment. This will highlight areas where your application might be vulnerable. One may wonder: if grep finds these instances, is that enough? The answer is straightforward: developers may assign parameters to local variables before using them in a redirect, which a basic grep search will miss.

For a more comprehensive evaluation, static analysis tools like Brakeman are often used. They can automatically detect unvalidated redirects and other security flaws. Running Brakeman against your codebase will provide a detailed list of potential vulnerabilities, including those related to CVE-2023-28362.

$ bundle exec brakeman
...snip...
== Warnings ==

Confidence: High
Category: Redirect
Check: Redirect
Message: Possible unprotected redirect
Code: redirect_to(params[:return_to] || root_path)
File: app/controllers/sessions_controller.rb
Line: 31
...snip...

Note the use of bundle exec; we could have also used the global brakeman executable, but running it through bundle exec ensures that we are using the specific version of Brakeman defined in our project’s Gemfile.lock, avoiding potential compatibility issues.

Long-Term Security Maintenance

Vulnerabilities like CVE-2023-28362 illustrate the ongoing cost of maintaining software. Frameworks are designed to provide secure defaults, but those defaults must be continuously updated as new attack vectors are discovered.

When you run bundle update to apply security patches, or perform a manual framework upgrade, you are effectively paying down technical debt. Organizations that prioritize regular, gradual version bumps avoid the compounding costs of deferred maintenance.

If your team lacks the bandwidth to handle continuous updates, establishing a predictable, ongoing codebase maintenance schedule is often a practical approach. This helps ensure that security patches are applied promptly and that the application remains secure against known vulnerabilities.

Ultimately, web security requires a proactive stance. By understanding the mechanics of vulnerabilities like CVE-2023-28362, applying the necessary patches, and implementing robust validation for user input, we can build applications that stand the test of time.

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