CVE-2020-5249 (Ruby, published 2020-03-02T16:15Z)
Description: value: In Puma (RubyGem) before 4.3.3 and 3.12.4, if an application using Puma allows untrusted input in an early-hints header, an attacker can use a carriage return character to end the header and inject malicious content, such as additional headers or an entirely new response body. This vulnerability is known as HTTP Response Splitting. While not an attack in itself, response splitting is a vector for several other attacks, such as cross-site scripting (XSS). This is related to CVE-2020-5247, which fixed this vulnerability but only for regular responses. This has been fixed in 4.3.3 and 3.12.4.
Severity: MEDIUM (CVSS v3 base score: 6.5)
Weakness: CWE-74 \u2014 Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
Repository: https://github.com/puma/puma
Files changed:
History.md
lib/puma/const.rb
lib/puma/server.rb
test/test_puma_server.rb
Commit message: HTTP Injection - fix bug + 1 more vector (#2136)

+ Fixes a problem in 4.3.2/3.12.3 where we were not splitting newlines in headers according to Rack spec
+ Fixes another vector for HTTP injection - early hints

Vulnerable code:
    CRLF_REGEX = /[\r\n]/.freeze
          # Checking to see if an attacker is trying to inject headers into the response
          headers.reject! { |_k, v| CRLF_REGEX =~ v.to_s }
  # https://github.com/ruby/ruby/commit/d9d4a28f1cdd05a0e8dabb36d747d40bbcc30f16
  def test_prevent_response_splitting_headers
    server_run app: ->(_) { [200, {'X-header' => "malicious\r\nCookie: hack"}, ["Hello"]] }
    refute_match 'hack', data
  def test_prevent_response_splitting_headers_cr
    server_run app: ->(_) { [200, {'X-header' => "malicious\rCookie: hack"}, ["Hello"]] }
    refute_match 'hack', data
  def test_prevent_response_splitting_headers_lf
    server_run app: ->(_) { [200, {'X-header' => "malicious\nCookie: hack"}, ["Hello"]] }
    refute_match 'hack', data

Fixed code:

## 4.3.3 and 3.12.4 / 2020-02-28
  * Bugfixes
    * Fix: Fixes a problem where we weren't splitting headers correctly on newlines (#2132)
  * Security
    * Fix: Prevent HTTP Response splitting via CR in early hints.

    HTTP_INJECTION_REGEX = /[\r\n]/.freeze
                  next if possible_header_injection?(v)
            next if possible_header_injection?(vs)
              next if possible_header_injection?(v)

    def possible_header_injection?(header_value)
      HTTP_INJECTION_REGEX =~ header_value.to_s
    end
    private :possible_header_injection?
  # Rack may pass a newline in a header expecting us to split it.
  def test_newline_splits
    server_run app: ->(_) { [200, {'X-header' => "first line\nsecond line"}, ["Hello"]] }


    assert_match "X-header: first line\r\nX-header: second line\r\n", data
  def test_newline_splits_in_early_hint
    server_run early_hints: true, app: ->(env) do
      env['rack.early_hints'].call({'X-header' => "first line\nsecond line"})
      [200, {}, ["Hello world!"]]
    end


    assert_match "X-header: first line\r\nX-header: second line\r\n", data
  # To comply with the Rack spec, we have to split header field values
  # containing newlines into multiple headers.
  def assert_does_not_allow_http_injection(app, opts = {})
    server_run(early_hints: opts[:early_hints], app: app)


    refute_match(/[\r\n]Cookie: hack[\r\n]/, data)
  end

  # HTTP Injection Tests
  #
  # Puma should prevent injection of CR and LF characters into headers, either as
  # CRLF or CR or LF, because browsers may interpret it at as a line end and
  # allow untrusted input in the header to split the header or start the
  # response body. While it's not documented anywhere and they shouldn't be doing
  # it, Chrome and curl recognize a lone CR as a line end. According to RFC,
  # clients SHOULD interpret LF as a line end for robustness, and CRLF is the
  # specced line end.
  #
  # There are three different tests because there are three ways to set header
  # content in Puma. Regular (rack env), early hints, and a special case for
  # overriding content-length.
  {"cr" => "\r", "lf" => "\n", "crlf" => "\r\n"}.each do |suffix, line_ending|
    # The cr-only case for the following test was CVE-2020-5247
    define_method("test_prevent_response_splitting_headers_#{suffix}") do
      app = ->(_) { [200, {'X-header' => "untrusted input#{line_ending}Cookie: hack"}, ["Hello"]] }
      assert_does_not_allow_http_injection(app)
    end

    define_method("test_prevent_response_splitting_headers_early_hint_#{suffix}") do
      app = ->(env) do
        env['rack.early_hints'].call("X-header" => "untrusted input#{line_ending}Cookie: hack")
        [200, {}, ["Hello"]]
      end
      assert_does_not_allow_http_injection(app, early_hints: true)
    end

    define_method("test_prevent_content_length_injection_#{suffix}") do
      app = ->(_) { [200, {'content-length' => "untrusted input#{line_ending}Cookie: hack"}, ["Hello"]] }
      assert_does_not_allow_http_injection(app)
    end