UpgradeRuby.com Logo

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

Resolving ReDoS Vulnerabilities (CVE-2023-22792) in Outdated Rails Apps


In January 2023, the Rails security team disclosed a vulnerability that looked, on the surface, like a routine bug.1 It was a flaw in how Rails parsed a common HTTP header – something that happens thousands of times a second in a busy application. The vulnerability, designated CVE-2023-22792, was a Regular Expression Denial of Service (ReDoS) flaw in ActionDispatch, the component responsible for routing requests in Rails.

Unlike many vulnerabilities that require exotic attack vectors, though, this one could be triggered by sending a specially crafted HTTP request to any publicly accessible Rails application running an affected version. An attacker didn’t need a user account, a special network position, or a complex exploit chain. All they needed was curl and a single, maliciously formed Content-Type header.

The vulnerability existed in the regular expression used to parse that header. An attacker could send a request containing patterns that caused catastrophic backtracking in the regex engine. This, in turn, could lock up a worker thread, consume all available CPU, and potentially bring down an entire application through resource exhaustion.

For teams maintaining legacy Rails applications, this CVE represents a perfect storm – it affects multiple Rails versions, requires no authentication to exploit, and can be triggered from anywhere on the internet. In this article, we’ll walk through how to identify, test for, and remediate this vulnerability in outdated Rails applications. We’ll cover everything from a quick version check to crafting a middleware patch for applications that can’t be upgraded.

Understanding the Vulnerability

CVE-2023-22792 is a ReDoS vulnerability in the ActionDispatch::Http::ContentSecurityPolicy module.2 The affected regular expression was used to validate and parse media type parameters in HTTP headers. Before we get into the specifics, though, let’s take a step back and discuss what a ReDoS attack actually is.

What is ReDoS?

Regular Expression Denial of Service attacks exploit the way certain regex patterns are evaluated. When a regex engine encounters a pattern with multiple overlapping paths to a match – common with nested quantifiers like (a+)+ or (a|ab)+ – it may try an exponential number of combinations to find a match. This is known as catastrophic backtracking. You can think of it as the regex engine getting lost in a maze of its own making, trying every possible path and never finding an exit.

For example, let’s consider a simplified version of the vulnerable pattern:

# Simplified representation of the vulnerable regex
/\A[a-z]+([;,]\s*[a-z]+=[^;,]+)*\z/i

When given a seemingly innocuous input like "text/html;a=" + ("!" * 50000), the regex engine must backtrack through a massive number of possible ways to match the repeated groups. This process consumes CPU time exponentially relative to the input length, and that’s the heart of the denial of service vector.

The Attack Vector

The vulnerability resided in how Rails parsed the Content-Type header. An attacker could send a POST request with a carefully constructed Content-Type that triggers this catastrophic backtracking.

You could execute this attack with a simple curl command:

curl -X POST https://yourapp.com/any-endpoint \
  -H "Content-Type: text/html;$(python -c 'print("a=!" * 10000)')"

This single request could lock up a Puma or Unicorn worker for seconds or even minutes. A handful of these requests, sent in parallel, could exhaust all available workers, making the application completely unresponsive to legitimate traffic.

Affected Versions

CVE-2023-22792 affects a wide range of Rails versions, spanning from 5.2 to 7.0:3

  • Rails 7.0.0 through 7.0.4
  • Rails 6.1.0 through 6.1.7
  • Rails 6.0.0 through 6.0.6
  • Rails 5.2.0 through 5.2.8.1

Of course, the Rails 5.2 series is no longer supported for security patches, which makes this vulnerability particularly challenging for applications running on that version. If you’re running any of the versions listed above, your application is vulnerable to remote denial of service attacks.

Identifying the Vulnerability in Your Application

Now that we understand the attack, let’s determine if your application is affected. This is a multi-step process: first, we’ll check the Rails version, and then we’ll write a test to prove the vulnerability exists in your environment.

Check Your Rails Version

First, you’ll need to determine which version of Rails you’re running. You can do this from the command line:

$ bundle exec rails -v
Rails 6.1.7

Alternatively, you can check your Gemfile.lock for the exact version of Rails your application is using.

$ grep -A 1 "rails (" Gemfile.lock
rails (6.1.7)
  actioncable (= 6.1.7)

If the version falls within the affected ranges we listed earlier, you need to take action.

Verify Vulnerable Code Paths

The vulnerability exists in actionpack, specifically in the code that processes request headers. While you could dive into the ActionDispatch source code to find the exact regular expression, it’s not strictly necessary.

$ bundle show actionpack
/path/to/gems/actionpack-6.1.7
$ cd /path/to/gems/actionpack-6.1.7
$ grep -r "content.*type" lib/action_dispatch/http/

If you’re running an affected version of Rails, it’s safe to assume you’re vulnerable. The most reliable way to be sure, though, is to write a test that reproduces the attack.

Test for the Vulnerability

You can write a simple integration test to verify your application’s vulnerability. This gives you a concrete way to prove the vulnerability exists and, later, to confirm that your fix has worked.

Create a new test file for this security check:

# test/security/redos_test.rb
require 'test_helper'

class RedosTest < ActionDispatch::IntegrationTest
  test "application is not vulnerable to CVE-2023-22792" do
    # Create a pathological Content-Type header
    malicious_content_type = "text/html;" + ("a=!" * 1000)

    start_time = Time.now

    begin
      post "/", headers: { "Content-Type" => malicious_content_type }
    rescue => e
      # We can ignore any exceptions here, since we only care about timing.
    end

    end_time = Time.now
    duration = end_time - start_time

    # If processing takes more than 1 second, we have a problem.
    # A non-vulnerable application should process this in milliseconds.
    assert duration < 1.0, "Request took #{duration}s, possible ReDoS vulnerability"
  end
end

Now, run the test from your terminal:

$ bundle exec rails test test/security/redos_test.rb

If the test fails with a message like Request took 5.2s, or if it seems to hang indefinitely, you have confirmed the vulnerability. A passing test, on the other hand, will likely show a duration of a few milliseconds.

Remediation Strategies

There are several ways to address this vulnerability. The best approach for you will depend on your application’s age, your team’s resources, and your deployment environment. We’ll walk through four different strategies, from a full Rails upgrade to a WAF-level block.

The most reliable and comprehensive fix is to upgrade to a patched version of Rails. This is the recommended approach because it ensures you receive the official fix from the Rails team and also keeps your application up-to-date with other security patches and bug fixes.

The patched versions are:4

  • Rails 7.0.4.1 or later
  • Rails 6.1.7.1 or later
  • Rails 6.0.6.1 or later

First, update your Gemfile to specify the new version:

# Gemfile
gem 'rails', '~> 6.1.7.1' # Or the appropriate version for your app

Then, run the bundle update command and any necessary Rails update tasks:

$ bundle update rails
$ bundle exec rails app:update
$ bundle exec rails test

After upgrading, be sure to rerun your security test. It should now pass quickly, confirming that the fix is in place.

Strategy 2: Patch ActionPack Directly

What if you can’t perform a full Rails upgrade right away? Perhaps the new version has breaking changes you’re not ready to address. In that case, you can often upgrade just the actionpack gem, which contains the vulnerable code.

You would add the specific actionpack version to your Gemfile:

# Gemfile
gem 'actionpack', '~> 6.1.7.1'

This approach, though, carries some risk. A newer version of actionpack might have subtle incompatibilities with an older version of railties or other Rails components. It’s not a common way to manage Rails dependencies, so you’ll need to test your application thoroughly after this change.

$ bundle update actionpack
$ bundle exec rails test

This can be a good short-term solution, but you should still plan for a full Rails upgrade.

Strategy 3: Apply a Middleware Patch

For applications that cannot be upgraded at all – for example, Rails 5.2 applications that are no longer receiving security updates – a middleware patch is a viable option. This involves writing a small piece of Rack middleware that inspects the Content-Type header before it reaches the vulnerable code in ActionDispatch.

Here’s an example of what that middleware might look like:

# lib/middleware/content_type_validator.rb
module Middleware
  class ContentTypeValidator
    MAX_CONTENT_TYPE_LENGTH = 256
    # This regex is intentionally simpler and safer than the vulnerable one.
    SAFE_CONTENT_TYPE_PATTERN = /\A[\w\/\+\-\.]+(?:;\s*[\w\-]+=[\w\-\.]+)*\z/i

    def initialize(app)
      @app = app
    end

    def call(env)
      content_type = env['CONTENT_TYPE']

      if content_type
        # First, reject any overly long Content-Type headers.
        if content_type.length > MAX_CONTENT_TYPE_LENGTH
          return [400, {'Content-Type' => 'text/plain'},
                  ['Bad Request: Content-Type header too long']]
        end

        # Next, reject headers with patterns that don't match our safe list.
        unless content_type.match?(SAFE_CONTENT_TYPE_PATTERN)
          return [400, {'Content-Type' => 'text/plain'},
                  ['Bad Request: Invalid Content-Type header']]
        end
      end

      @app.call(env)
    end
  end
end

To use this middleware, you’ll need to load it and insert it into the middleware stack in your config/application.rb file.

# config/application.rb
require_relative '../lib/middleware/content_type_validator'

module YourApp
  class Application < Rails::Application
    # ...
    config.middleware.insert_before ActionDispatch::ContentSecurityPolicy::Middleware,
                                    Middleware::ContentTypeValidator
  end
end

This approach is a form of defensive programming. It’s not a true fix for the underlying vulnerability, but it effectively blocks the exploit before it can do any harm. Of course, this approach has its own risks – the middleware must be placed early enough in the stack to intercept requests, and the regular expression it uses must itself be immune to ReDoS attacks.

Strategy 4: Web Application Firewall Rules

If you’re using a Web Application Firewall (WAF) like AWS WAF, Cloudflare, or ModSecurity, you may be able to create rules to block requests with suspicious Content-Type headers at the edge, before they even reach your application.

This is another layer of defense, not a replacement for patching your application. WAF rules can be blunt instruments and might cause false positives. For example, a rule that blocks any Content-Type with a semicolon could break legitimate requests that include a charset, like text/html; charset=utf-8.

A more targeted and safer approach is to block requests where the Content-Type header is unusually long. The pathological Content-Type strings used in ReDoS attacks are often thousands of characters long, while legitimate headers are typically much shorter.

Here’s an example of an AWS WAF rule that blocks requests with a Content-Type header longer than 256 characters:

{
  "Name": "BlockLongContentType",
  "Priority": 1,
  "Statement": {
    "SizeConstraintStatement": {
      "FieldToMatch": {
        "SingleHeader": {
          "Name": "content-type"
        }
      },
      "ComparisonOperator": "GT",
      "Size": 256,
      "TextTransformations": [{"Priority": 0, "Type": "NONE"}]
    }
  },
  "Action": {"Block": {}},
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "BlockLongContentType"
  }
}

This is a reasonable defense-in-depth measure, but it shouldn’t be your only line of defense. It’s always better to patch the vulnerability in your application code if you can.

Testing Your Fix

After you’ve applied a remediation strategy, the next step is to verify that it works. You should do this with both your automated test and a manual check.

Automated Test

First, rerun the security test we wrote earlier:

$ bundle exec rails test test/security/redos_test.rb

The test, which previously failed or hung, should now pass in a fraction of a second. This is your first confirmation that the fix is working as expected.

Manual Verification

Next, it’s a good idea to perform a manual check with curl to see how your application behaves in a more realistic scenario.

# This should either return quickly or be blocked by your WAF/middleware.
$ time curl -X POST https://your-app.com/any-endpoint \
  -H "Content-Type: text/html;$(python -c 'print("a=!" * 10000)')" \
  -w "\nTime: %{time_total}s\n"

The response time should be well under a second. If you’ve used a middleware patch or a WAF rule, you should see a 400 Bad Request or a similar error from your edge network. If you upgraded Rails, the request should simply complete quickly without causing a CPU spike.

Load Testing

If you have the ability to do so, a brief load test can be a valuable final step. This can help you ensure that your fix – especially a custom middleware or WAF rule – doesn’t inadvertently degrade performance for legitimate traffic.

# Using Apache Bench as an example
$ ab -n 1000 -c 10 -H "Content-Type: application/json" \
  -p payload.json https://your-app.com/api/endpoint

This is likely not a necessary step for a simple Rails version upgrade, but it’s worth considering for the other remediation strategies.

Long-term Recommendations

CVE-2023-22792 is just one vulnerability among many. The real work of software maintenance isn’t just reacting to the latest fire, but building a process that protects you from future ones. Here are a few recommendations for a more proactive security posture.

Adopt a Proactive Security Posture

  1. Subscribe to Security Mailing Lists: You can’t patch vulnerabilities you don’t know about. Monitor the Rails security mailing list5 and enable GitHub security alerts for your repositories.

  2. Automate Dependency Scanning: Use a tool like bundler-audit in your Continuous Integration (CI) pipeline.6 This will automatically check your dependencies for known vulnerabilities on every commit.

    # Install the gem
    $ gem install bundler-audit
    # Run the check
    $ bundle audit check --update

    You can add this to your CI configuration to make it part of your standard workflow.

  3. Establish an Upgrade Cadence: Don’t wait for a vulnerability to force your hand. Schedule time for regular Rails upgrades – perhaps quarterly, or at minimum when new major versions are released. This turns upgrades from a reactive scramble into a predictable part of your development cycle.

  4. Maintain a Staging Environment: Always test security patches and version upgrades in a staging environment that mirrors production. This is where you can catch unexpected side effects before they impact your users.

Plan for End of Life

If you’re running Rails 5.2 or earlier, you are on an unsupported version of the framework. This means that new vulnerabilities like CVE-2023-22792 may never be officially patched for your version of Rails. This is a significant risk, and it’s one you should actively plan to mitigate.

You’ll need to create a migration plan to a supported version:

  1. Assess Your Technical Debt: Identify the gems, dependencies, and deprecated APIs that are holding you back.
  2. Budget for the Upgrade: Allocate dedicated development time and resources. Upgrades are real work and need to be treated as such.
  3. Use Dual-Boot Testing: Tools like bootboot can help you test your application against both your current and target Rails versions simultaneously, which can make the upgrade process much smoother.7
  4. Communicate with Stakeholders: Help your leadership team understand the security and compliance risks of running unsupported software. Frame it not as a technical chore, but as a necessary part of doing business securely.

Conclusion

CVE-2023-22792 is a fascinating case study in how seemingly innocuous code – in this case, parsing an HTTP header – can become a critical vulnerability. The unexpected behavior of a regular expression engine under adversarial input turned a routine operation into a potent denial of service attack vector.

For those of us maintaining Rails applications, the lesson is clear: staying current with security patches is not optional, it’s a fundamental part of our jobs. Whether you choose to upgrade Rails entirely, patch individual components, or implement defensive measures like middleware and WAF rules, you must take action to protect your application from known, and easily exploitable, vulnerabilities.

If you’re on an unsupported Rails version, the urgency is even greater. Every day you remain on an unpatched version is a day your application is exposed to attacks that can be launched by anyone with an internet connection and a curl command.

The good news, though, is that this vulnerability has well-documented and effective fixes. The hard part isn’t the technical solution, but rather prioritizing the work, testing it thoroughly, and deploying with confidence. That is the work of maintaining software in a world of ever-evolving threats – and it’s work that must be done.

Footnotes

  1. Ruby on Rails Security Team. “[CVE-2023-22792] Possible ReDoS based DoS vulnerability in Action Dispatch.” Ruby on Rails Discussions, 17 Jan. 2023, https://discuss.rubyonrails.org/t/cve-2023-22792-possible-redos-based-dos-vulnerability-in-action-dispatch/82115.

  2. National Institute of Standards and Technology (NIST). “CVE-2023-22792 Detail.” National Vulnerability Database, 17 Feb. 2023, https://nvd.nist.gov/vuln/detail/CVE-2023-22792.

  3. Ruby on Rails API Documentation. “ActionDispatch Class.” Ruby on Rails API, https://api.rubyonrails.org/classes/ActionDispatch.html.

  4. Debian Security Team. “DSA-5372-1 rails security update.” Debian Security Announcements, 13 Mar. 2023, https://www.debian.org/security/2023/dsa-5372.

  5. Ruby on Rails. “Ruby on Rails: Security.” Google Groups. https://groups.google.com/g/rubyonrails-security.

  6. RubySec Team. “bundler-audit: Patch-level verification for Bundler.” GitHub Repository, https://github.com/rubysec/bundler-audit.

  7. Shopify Engineering. “Bootboot: Dual-boot testing for Rails applications.” GitHub Repository, https://github.com/Shopify/bootboot.

You May Also Like