HTTP Response Splitting (Early Hints) in Puma
Description
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.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Puma HTTP response splitting via early-hints header allows injection of CR characters, enabling header/body manipulation when untrusted input is used.
CVE-2020-5249 is an HTTP response splitting vulnerability in Puma (RubyGem) affecting versions before 4.3.3 and 3.12.4. The flaw arises because Puma's handling of early-hints headers does not properly sanitize carriage return (CR) characters. An attacker who can supply untrusted input into an early-hints header value can inject a CR character to prematurely terminate the header and control subsequent headers or the response body [1][2].
For exploitation, the application must pass user-controllable data into an early-hints header. The attacker does not need authentication if the application exposes such input. The CR character allows the attacker to break out of the header context and inject arbitrary headers (e.g., Set-Cookie) or even an entire fake HTTP response body, enabling response splitting [3][4].
This vulnerability can be chained with other attacks, such as cross-site scripting (XSS). By injecting malicious content into the response, an attacker can modify the page content returned to the victim's browser, potentially stealing cookies or performing actions on behalf of the user [2][4]. Note that a related issue, CVE-2020-5247, fixed response splitting in regular (non-early-hint) responses, but the early-hints path remained vulnerable [1].
The issue is resolved in Puma versions 4.3.3 and 3.12.4, which properly validate and reject CR and LF characters in early-hints headers [1][3]. Users are strongly encouraged to upgrade immediately. No known workarounds exist apart from ensuring untrusted input is never placed in early-hints headers.
AI Insight generated on May 21, 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.
| Package | Affected versions | Patched versions |
|---|---|---|
pumaRubyGems | < 3.12.4 | 3.12.4 |
pumaRubyGems | >= 4.0.0, < 4.3.3 | 4.3.3 |
Affected products
12- ghsa-coords11 versionspkg:gem/pumapkg:rpm/opensuse/rmt-server&distro=openSUSE%20Leap%2015.1pkg:rpm/opensuse/rmt-server&distro=openSUSE%20Leap%2015.2pkg:rpm/suse/rmt-server&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015-ESPOSpkg:rpm/suse/rmt-server&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015-LTSSpkg:rpm/suse/rmt-server&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Public%20Cloud%2015%20SP1pkg:rpm/suse/rmt-server&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Public%20Cloud%2015%20SP2pkg:rpm/suse/rmt-server&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Server%20Applications%2015%20SP1pkg:rpm/suse/rmt-server&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Server%20Applications%2015%20SP2pkg:rpm/suse/rmt-server&distro=SUSE%20Linux%20Enterprise%20Server%2015-LTSSpkg:rpm/suse/rmt-server&distro=SUSE%20Linux%20Enterprise%20Server%20for%20SAP%20Applications%2015
< 3.12.4+ 10 more
- (no CPE)range: < 3.12.4
- (no CPE)range: < 2.6.5-lp151.2.18.2
- (no CPE)range: < 2.6.5-lp152.2.3.1
- (no CPE)range: < 2.6.5-3.34.1
- (no CPE)range: < 2.6.5-3.34.1
- (no CPE)range: < 2.6.5-3.18.1
- (no CPE)range: < 2.6.5-3.3.1
- (no CPE)range: < 2.6.5-3.18.1
- (no CPE)range: < 2.6.5-3.3.1
- (no CPE)range: < 2.6.5-3.34.1
- (no CPE)range: < 2.6.5-3.34.1
- puma/Pumav5Range: < 3.12.4
Patches
1c22712fc9328HTTP Injection - fix bug + 1 more vector (#2136)
4 files changed · +71 −13
History.md+7 −0 modified@@ -23,6 +23,13 @@ * Simplify `Runner#start_control` URL parsing (#2111) * Removed the IOBuffer extension and replaced with Ruby (#1980) + +## 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. + ## 4.3.2 and 3.12.3 / 2020-02-27 * Security
lib/puma/const.rb+1 −1 modified@@ -228,7 +228,7 @@ module Const COLON = ": ".freeze NEWLINE = "\n".freeze - CRLF_REGEX = /[\r\n]/.freeze + HTTP_INJECTION_REGEX = /[\r\n]/.freeze HIJACK_P = "rack.hijack?".freeze HIJACK = "rack.hijack".freeze
lib/puma/server.rb+8 −2 modified@@ -663,6 +663,7 @@ def handle_request(req, lines) headers.each_pair do |k, vs| if vs.respond_to?(:to_s) && !vs.to_s.empty? vs.to_s.split(NEWLINE).each do |v| + next if possible_header_injection?(v) fast_write client, "#{k}: #{v}\r\n" end else @@ -687,8 +688,6 @@ def handle_request(req, lines) status, headers, res_body = @app.call(env) return :async if req.hijacked - # Checking to see if an attacker is trying to inject headers into the response - headers.reject! { |_k, v| CRLF_REGEX =~ v.to_s } status = status.to_i @@ -766,6 +765,7 @@ def handle_request(req, lines) headers.each do |k, vs| case k.downcase when CONTENT_LENGTH2 + next if possible_header_injection?(vs) content_length = vs next when TRANSFER_ENCODING @@ -778,6 +778,7 @@ def handle_request(req, lines) if vs.respond_to?(:to_s) && !vs.to_s.empty? vs.to_s.split(NEWLINE).each do |v| + next if possible_header_injection?(v) lines.append k, colon, v, line_ending end else @@ -1048,5 +1049,10 @@ def self.current def shutting_down? @status == :stop || @status == :restart end + + def possible_header_injection?(header_value) + HTTP_INJECTION_REGEX =~ header_value.to_s + end + private :possible_header_injection? end end
test/test_puma_server.rb+55 −10 modified@@ -772,22 +772,67 @@ def test_open_connection_wait_no_queue test_open_connection_wait end - # https://github.com/ruby/ruby/commit/d9d4a28f1cdd05a0e8dabb36d747d40bbcc30f16 - def test_prevent_response_splitting_headers - server_run app: ->(_) { [200, {'X-header' => "malicious\r\nCookie: hack"}, ["Hello"]] } + # 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"]] } + data = send_http_and_read "HEAD / HTTP/1.0\r\n\r\n" - refute_match 'hack', data + + assert_match "X-header: first line\r\nX-header: second line\r\n", data end - def test_prevent_response_splitting_headers_cr - server_run app: ->(_) { [200, {'X-header' => "malicious\rCookie: hack"}, ["Hello"]] } + 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 + data = send_http_and_read "HEAD / HTTP/1.0\r\n\r\n" - refute_match 'hack', data + + assert_match "X-header: first line\r\nX-header: second line\r\n", data end - def test_prevent_response_splitting_headers_lf - server_run app: ->(_) { [200, {'X-header' => "malicious\nCookie: hack"}, ["Hello"]] } + # 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) + data = send_http_and_read "HEAD / HTTP/1.0\r\n\r\n" - refute_match 'hack', data + + 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 end end
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
13- github.com/advisories/GHSA-33vf-4xgg-9r58ghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/BMJ3CGZ3DLBJ5WUUKMI5ZFXFJQMXJZIK/mitrevendor-advisoryx_refsource_FEDORA
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/DIHVO3CQMU7BZC7FCTSRJ33YDNS3GFPK/mitrevendor-advisoryx_refsource_FEDORA
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/NJ3LL5F5QADB6LM46GXZETREAKZMQNRD/mitrevendor-advisoryx_refsource_FEDORA
- nvd.nist.gov/vuln/detail/CVE-2020-5249ghsaADVISORY
- github.com/puma/puma/commit/c22712fc93284a45a93f9ad7023888f3a65524f3ghsax_refsource_MISCWEB
- github.com/puma/puma/security/advisories/GHSA-33vf-4xgg-9r58ghsax_refsource_CONFIRMWEB
- github.com/puma/puma/security/advisories/GHSA-84j7-475p-hp8vghsax_refsource_MISCWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/puma/CVE-2020-5249.ymlghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/BMJ3CGZ3DLBJ5WUUKMI5ZFXFJQMXJZIKghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DIHVO3CQMU7BZC7FCTSRJ33YDNS3GFPKghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/NJ3LL5F5QADB6LM46GXZETREAKZMQNRDghsaWEB
- owasp.org/www-community/attacks/HTTP_Response_Splittingghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.