VYPR
Moderate severityNVD Advisory· Published Jun 25, 2025· Updated Jun 25, 2025

Ruby WEBrick read_header HTTP Request Smuggling Vulnerability

CVE-2025-6442

Description

Ruby WEBrick read_header HTTP Request Smuggling Vulnerability. This vulnerability allows remote attackers to smuggle arbitrary HTTP requests on affected installations of Ruby WEBrick. This issue is exploitable when the product is deployed behind an HTTP proxy that fulfills specific conditions.

The specific flaw exists within the read_headers method. The issue results from the inconsistent parsing of terminators of HTTP headers. An attacker can leverage this vulnerability to smuggle arbitrary HTTP requests. Was ZDI-CAN-21876.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Ruby WEBrick's read_headers method inconsistently parses HTTP header terminators, enabling remote attackers to smuggle arbitrary HTTP requests when the server is behind a specific HTTP proxy.

Vulnerability

Details

CVE-2025-6442 is an HTTP request smuggling vulnerability in Ruby's WEBrick HTTP server toolkit. The flaw resides in the read_headers method, which inconsistently parses terminators of HTTP headers [1][2]. This inconsistency allows an attacker to craft HTTP requests that are interpreted differently by WEBrick and a front-end HTTP proxy, leading to request smuggling.

Exploitation

Conditions

Exploitation requires WEBrick to be deployed behind an HTTP proxy that meets specific conditions [2]. The attacker does not need authentication and can exploit the vulnerability remotely. By sending specially crafted requests, the attacker can cause the proxy and WEBrick to disagree on request boundaries, enabling the smuggling of arbitrary HTTP requests.

Impact

Successful exploitation allows an attacker to smuggle arbitrary HTTP requests, potentially leading to cache poisoning, request hijacking, or bypassing security controls. The CVSS v3.1 score is 6.5 (Medium), with low confidentiality impact and high integrity impact [2].

Mitigation

Ruby has released an update to address this vulnerability [4]. Users are advised to upgrade WEBrick to the latest version. The Ruby Security Advisory Database provides further details on affected versions and remediation steps.

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
webrickRubyGems
< 1.8.21.8.2

Affected products

20

Patches

1
ee60354bcb84

Require CRLF line endings in request line and headers

https://github.com/ruby/webrickJeremy EvansJun 25, 2024via ghsa
4 files changed · +133 32
  • lib/webrick/httprequest.rb+2 2 modified
    @@ -458,7 +458,7 @@ def read_request_line(socket)
           end
     
           @request_time = Time.now
    -      if /^(\S+)\s+(\S++)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line
    +      if /^(\S+) (\S++)(?: HTTP\/(\d+\.\d+))?\r\n/mo =~ @request_line
             @request_method = $1
             @unparsed_uri   = $2
             @http_version   = HTTPVersion.new($3 ? $3 : "0.9")
    @@ -471,7 +471,7 @@ def read_request_line(socket)
         def read_header(socket)
           if socket
             while line = read_line(socket)
    -          break if /\A(#{CRLF}|#{LF})\z/om =~ line
    +          break if /\A#{CRLF}\z/om =~ line
               if (@request_bytes += line.bytesize) > MAX_HEADER_LENGTH
                 raise HTTPStatus::RequestEntityTooLarge, 'headers too large'
               end
    
  • lib/webrick/httputils.rb+6 4 modified
    @@ -173,16 +173,18 @@ def parse_header(raw)
           field = nil
           raw.each_line{|line|
             case line
    -        when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):(.*?)\z/om
    -          field, value = $1, $2.strip
    +        when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):([^\r\n\0]*?)\r\n\z/om
    +          field, value = $1, $2
               field.downcase!
               header[field] = HEADER_CLASSES[field].new unless header.has_key?(field)
               header[field] << value
    -        when /^\s+(.*?)/om
    -          value = line.strip
    +        when /^\s+([^\r\n\0]*?)\r\n/om
               unless field
                 raise HTTPStatus::BadRequest, "bad header '#{line}'."
               end
    +          value = line
    +          value.lstrip!
    +          value.slice!(-2..-1)
               header[field][-1] << " " << value
             else
               raise HTTPStatus::BadRequest, "bad header '#{line}'."
    
  • test/webrick/test_filehandler.rb+1 1 modified
    @@ -33,7 +33,7 @@ def make_range_request(range_spec)
           Range: #{range_spec}
     
         END_OF_REQUEST
    -    return StringIO.new(msg.gsub(/^ {6}/, ""))
    +    return StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n"))
       end
     
       def make_range_response(file, range_spec)
    
  • test/webrick/test_httprequest.rb+124 25 modified
    @@ -11,7 +11,7 @@ def teardown
     
       def test_simple_request
         msg = <<-_end_of_message_
    -GET /
    +GET /\r
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
         req.parse(StringIO.new(msg))
    @@ -24,7 +24,7 @@ def test_parse_09
           foobar    # HTTP/0.9 request don't have header nor entity body.
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -    req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         assert_equal("GET", req.request_method)
         assert_equal("/", req.unparsed_uri)
         assert_equal(WEBrick::HTTPVersion.new("0.9"), req.http_version)
    @@ -41,7 +41,7 @@ def test_parse_10
     
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -    req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         assert_equal("GET", req.request_method)
         assert_equal("/", req.unparsed_uri)
         assert_equal(WEBrick::HTTPVersion.new("1.0"), req.http_version)
    @@ -58,7 +58,7 @@ def test_parse_11
     
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -    req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         assert_equal("GET", req.request_method)
         assert_equal("/path", req.unparsed_uri)
         assert_equal("", req.script_name)
    @@ -77,7 +77,7 @@ def test_request_uri_too_large
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
         assert_raise(WEBrick::HTTPStatus::RequestURITooLarge){
    -      req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +      req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         }
       end
     
    @@ -89,11 +89,101 @@ def test_invalid_content_length_header
           _end_of_message_
           req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
           assert_raise(WEBrick::HTTPStatus::BadRequest){
    -        req.parse(StringIO.new(msg.gsub(/^ {8}/, "")))
    +        req.parse(StringIO.new(msg.gsub(/^ {8}/, "").gsub("\n", "\r\n")))
           }
         end
       end
     
    +  def test_bare_lf_request_line
    +    msg = <<-_end_of_message_
    +      GET / HTTP/1.1
    +      Content-Length: 0\r
    +      \r
    +    _end_of_message_
    +    req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    +    assert_raise(WEBrick::HTTPStatus::BadRequest){
    +      req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    }
    +  end
    +
    +  def test_bare_lf_header
    +    msg = <<-_end_of_message_
    +      GET / HTTP/1.1\r
    +      Content-Length: 0
    +      \r
    +    _end_of_message_
    +    req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    +    assert_raise(WEBrick::HTTPStatus::BadRequest){
    +      req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    }
    +  end
    +
    +  def test_bare_cr_request_line
    +    msg = <<-_end_of_message_
    +      GET / HTTP/1.1\r\r
    +      Content-Length: 0\r
    +      \r
    +    _end_of_message_
    +    req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    +    assert_raise(WEBrick::HTTPStatus::BadRequest){
    +      req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    }
    +  end
    +
    +  def test_bare_cr_header
    +    msg = <<-_end_of_message_
    +      GET / HTTP/1.1\r
    +      Content-Type: foo\rbar\r
    +      \r
    +    _end_of_message_
    +    req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    +    assert_raise(WEBrick::HTTPStatus::BadRequest){
    +      req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    }
    +  end
    +
    +  def test_invalid_request_lines
    +    msg = <<-_end_of_message_
    +      GET  / HTTP/1.1\r
    +      Content-Length: 0\r
    +      \r
    +    _end_of_message_
    +    req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    +    assert_raise(WEBrick::HTTPStatus::BadRequest){
    +      req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    }
    +
    +    msg = <<-_end_of_message_
    +      GET /  HTTP/1.1\r
    +      Content-Length: 0\r
    +      \r
    +    _end_of_message_
    +    req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    +    assert_raise(WEBrick::HTTPStatus::BadRequest){
    +      req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    }
    +
    +    msg = <<-_end_of_message_
    +      GET /\r HTTP/1.1\r
    +      Content-Length: 0\r
    +      \r
    +    _end_of_message_
    +    req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    +    assert_raise(WEBrick::HTTPStatus::BadRequest){
    +      req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    }
    +
    +    msg = <<-_end_of_message_
    +      GET / HTTP/1.1 \r
    +      Content-Length: 0\r
    +      \r
    +    _end_of_message_
    +    req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    +    assert_raise(WEBrick::HTTPStatus::BadRequest){
    +      req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    }
    +  end
    +
       def test_duplicate_content_length_header
         msg = <<-_end_of_message_
           GET / HTTP/1.1
    @@ -102,7 +192,7 @@ def test_duplicate_content_length_header
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
         assert_raise(WEBrick::HTTPStatus::BadRequest){
    -      req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +      req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         }
       end
     
    @@ -118,13 +208,13 @@ def test_parse_headers
           Accept-Language: en;q=0.5, *; q=0
           Accept-Language: ja
           Content-Type: text/plain
    -      Content-Length: 7
    +      Content-Length: 8
           X-Empty-Header:
     
           foobar
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -    req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         assert_equal(
           URI.parse("http://test.ruby-lang.org:8080/path"), req.request_uri)
         assert_equal("test.ruby-lang.org", req.host)
    @@ -135,9 +225,9 @@ def test_parse_headers
           req.accept)
         assert_equal(%w(gzip compress identity *), req.accept_encoding)
         assert_equal(%w(ja en *), req.accept_language)
    -    assert_equal(7, req.content_length)
    +    assert_equal(8, req.content_length)
         assert_equal("text/plain", req.content_type)
    -    assert_equal("foobar\n", req.body)
    +    assert_equal("foobar\r\n", req.body)
         assert_equal("", req["x-empty-header"])
         assert_equal(nil, req["x-no-header"])
         assert(req.query.empty?)
    @@ -146,22 +236,22 @@ def test_parse_headers
       def test_parse_header2()
         msg = <<-_end_of_message_
           POST /foo/bar/../baz?q=a HTTP/1.0
    -      Content-Length: 9
    +      Content-Length: 10
           User-Agent:
             FOO   BAR
             BAZ
     
           hogehoge
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -    req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         assert_equal("POST", req.request_method)
         assert_equal("/foo/baz", req.path)
         assert_equal("", req.script_name)
         assert_equal("/foo/baz", req.path_info)
    -    assert_equal("9", req['content-length'])
    +    assert_equal("10", req['content-length'])
         assert_equal("FOO   BAR BAZ", req['user-agent'])
    -    assert_equal("hogehoge\n", req.body)
    +    assert_equal("hogehoge\r\n", req.body)
       end
     
       def test_parse_headers3
    @@ -171,7 +261,7 @@ def test_parse_headers3
     
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -    req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         assert_equal(URI.parse("http://test.ruby-lang.org/path"), req.request_uri)
         assert_equal("test.ruby-lang.org", req.host)
         assert_equal(80, req.port)
    @@ -182,7 +272,7 @@ def test_parse_headers3
     
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -    req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         assert_equal(URI.parse("http://192.168.1.1/path"), req.request_uri)
         assert_equal("192.168.1.1", req.host)
         assert_equal(80, req.port)
    @@ -193,7 +283,7 @@ def test_parse_headers3
     
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -    req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         assert_equal(URI.parse("http://[fe80::208:dff:feef:98c7]/path"),
                      req.request_uri)
         assert_equal("[fe80::208:dff:feef:98c7]", req.host)
    @@ -205,7 +295,7 @@ def test_parse_headers3
     
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -    req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         assert_equal(URI.parse("http://192.168.1.1:8080/path"), req.request_uri)
         assert_equal("192.168.1.1", req.host)
         assert_equal(8080, req.port)
    @@ -216,7 +306,7 @@ def test_parse_headers3
     
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -    req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         assert_equal(URI.parse("http://[fe80::208:dff:feef:98c7]:8080/path"),
                      req.request_uri)
         assert_equal("[fe80::208:dff:feef:98c7]", req.host)
    @@ -231,7 +321,7 @@ def test_parse_get_params
     
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -    req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         query = req.query
         assert_equal("1", query["foo"])
         assert_equal(["1", "2", "3"], query["foo"].to_ary)
    @@ -251,7 +341,7 @@ def test_parse_post_params
           #{param}
         _end_of_message_
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -    req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +    req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
         query = req.query
         assert_equal("1", query["foo"])
         assert_equal(["1", "2", "3"], query["foo"].to_ary)
    @@ -270,6 +360,7 @@ def test_chunked
     
         _end_of_message_
         msg.gsub!(/^ {6}/, "")
    +    msg.gsub!("\n", "\r\n")
         File.open(__FILE__){|io|
           while chunk = io.read(100)
             msg << chunk.size.to_s(16) << crlf
    @@ -335,6 +426,7 @@ def test_forwarded
     
         _end_of_message_
         msg.gsub!(/^ {6}/, "")
    +    msg.gsub!("\n", "\r\n")
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
         req.parse(StringIO.new(msg))
         assert_equal("server.example.com", req.server_name)
    @@ -355,6 +447,7 @@ def test_forwarded
     
         _end_of_message_
         msg.gsub!(/^ {6}/, "")
    +    msg.gsub!("\n", "\r\n")
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
         req.parse(StringIO.new(msg))
         assert_equal("server.example.com", req.server_name)
    @@ -377,6 +470,7 @@ def test_forwarded
     
         _end_of_message_
         msg.gsub!(/^ {6}/, "")
    +    msg.gsub!("\n", "\r\n")
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
         req.parse(StringIO.new(msg))
         assert_equal("server.example.com", req.server_name)
    @@ -399,6 +493,7 @@ def test_forwarded
     
         _end_of_message_
         msg.gsub!(/^ {6}/, "")
    +    msg.gsub!("\n", "\r\n")
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
         req.parse(StringIO.new(msg))
         assert_equal("server1.example.com", req.server_name)
    @@ -421,6 +516,7 @@ def test_forwarded
     
         _end_of_message_
         msg.gsub!(/^ {6}/, "")
    +    msg.gsub!("\n", "\r\n")
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
         req.parse(StringIO.new(msg))
         assert_equal("server1.example.com", req.server_name)
    @@ -443,6 +539,7 @@ def test_forwarded
     
         _end_of_message_
         msg.gsub!(/^ {6}/, "")
    +    msg.gsub!("\n", "\r\n")
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
         req.parse(StringIO.new(msg))
         assert_equal("server1.example.com", req.server_name)
    @@ -460,6 +557,7 @@ def test_continue_sent
     
         _end_of_message_
         msg.gsub!(/^ {6}/, "")
    +    msg.gsub!("\n", "\r\n")
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
         req.parse(StringIO.new(msg))
         assert req['expect']
    @@ -476,6 +574,7 @@ def test_continue_not_sent
     
         _end_of_message_
         msg.gsub!(/^ {6}/, "")
    +    msg.gsub!("\n", "\r\n")
         req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
         req.parse(StringIO.new(msg))
         assert !req['expect']
    @@ -495,7 +594,7 @@ def test_bad_messages
         _end_of_message_
         assert_raise(WEBrick::HTTPStatus::LengthRequired){
           req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -      req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +      req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
           req.body
         }
     
    @@ -508,7 +607,7 @@ def test_bad_messages
         _end_of_message_
         assert_raise(WEBrick::HTTPStatus::BadRequest){
           req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -      req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +      req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
           req.body
         }
     
    @@ -521,7 +620,7 @@ def test_bad_messages
         _end_of_message_
         assert_raise(WEBrick::HTTPStatus::NotImplemented){
           req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
    -      req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
    +      req.parse(StringIO.new(msg.gsub(/^ {6}/, "").gsub("\n", "\r\n")))
           req.body
         }
       end
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.