Header normalization allows for client to clobber proxy set headers in Puma
Description
Puma is a Ruby/Rack web server built for parallelism. In affected versions clients could clobber values set by intermediate proxies (such as X-Forwarded-For) by providing a underscore version of the same header (X-Forwarded_For). Any users relying on proxy set variables is affected. v6.4.3/v5.6.9 now discards any headers using underscores if the non-underscore version also exists. Effectively, allowing the proxy defined headers to always win. Users are advised to upgrade. Nginx has a underscores_in_headers configuration variable to discard these headers at the proxy level as a mitigation. Any users that are implicitly trusting the proxy defined headers for security should immediately cease doing so until upgraded to the fixed versions.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Puma servers allow client-provided underscore headers (e.g., X-Forwarded_For) to override proxy-set values, bypassing IP-based security controls.
Vulnerability
Overview
CVE-2024-45614 is a header injection vulnerability in the Puma Ruby web server (versions prior to 6.4.3 and 5.6.9). The root cause lies in how Puma normalizes HTTP header names: it converts hyphens to underscores but does not check for existing underscore-form headers when a hyphen-form header (like X-Forwarded-For) is also present. This allows a malicious client to send a header with an underscore in place of a hyphen (e.g., X-Forwarded_For) and clobber the value set by an intermediate proxy, such as a load balancer or reverse proxy adding the real client IP via X-Forwarded-For. The normalization process treats both variants as distinct, but the application may trust the proxy-set value; however, the client-supplied underscore version can overwrite it due to the way headers are parsed and stored in the Rack environment [3].
Attack
Vector and Exploitation
An attacker with the ability to craft HTTP requests can exploit this flaw by sending a request containing the underscore variant of a header that a proxy is expected to set (e.g., X-Forwarded_For). No special authentication is required—any client able to connect to the Puma server directly or through a proxy can attempt this. The attack surface is particularly impactful for applications that rely on proxy-set headers for security decisions, such as IP-based access controls, logging, or rate limiting. For example, an attacker could spoof their IP address by sending X-Forwarded_For: 127.0.0.1 alongside a proxy-set X-Forwarded-For, potentially bypassing IP allowlists or masking their true origin [1]. The Nginx underscores_in_headers directive is noted as a mitigation at the proxy level, but it is not enabled by default, leaving many deployments exposed [1].
Impact
If exploited, an attacker can inject arbitrary values into headers that the application trusts for security or functionality, leading to potential authentication bypass, privilege escalation, or information disclosure. For instance, using a spoofed X-Forwarded-For, an attacker could mimic a trusted internal IP to access restricted resources, or manipulate other proxy-dependent logic such as X-Forwarded-Proto or X-Forwarded-Host to alter application behavior. The impact is rated with a CVSS v4.0 score pending, but the description underscores that all users who implicitly trust proxy-set headers for security should immediately reassess that trust [3].
Mitigation and
Remediation
Puma has released fixed versions 6.4.3 and 5.6.9 that discard underscore headers when a corresponding hyphen-form header already exists, ensuring proxy-set values always take precedence [2]. The fix is implemented in the HTTP parsing layer and the req_env_post_parse method, comparing header names after normalization and prioritizing the proxy-set variant [4]. Users are strongly advised to upgrade to the patched versions. As a temporary workaround, administrators can configure Nginx to set underscores_in_headers off; to reject underscore-containing headers entirely at the proxy level [1]. Additionally, any application logic that relies on proxy headers for security should be hardened to not trust client-controllable header values.
AI Insight generated on May 20, 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 | < 5.6.9 | 5.6.9 |
pumaRubyGems | >= 6.0.0, < 6.4.3 | 6.4.3 |
Affected products
127- osv-coords126 versionspkg:apk/chainguard/gitaly-config-17.4pkg:apk/chainguard/gitaly-config-18.1pkg:apk/chainguard/gitaly-config-18.2pkg:apk/chainguard/gitlab-base-17.4pkg:apk/chainguard/gitlab-base-18.1pkg:apk/chainguard/gitlab-base-18.2pkg:apk/chainguard/gitlab-certificates-17.4pkg:apk/chainguard/gitlab-certificates-18.1pkg:apk/chainguard/gitlab-certificates-18.2pkg:apk/chainguard/gitlab-cfssl-self-sign-scripts-17.4pkg:apk/chainguard/gitlab-cfssl-self-sign-scripts-18.1pkg:apk/chainguard/gitlab-cfssl-self-sign-scripts-18.2pkg:apk/chainguard/gitlab-cng-17.4pkg:apk/chainguard/gitlab-cng-18.1pkg:apk/chainguard/gitlab-cng-18.2pkg:apk/chainguard/gitlab-container-registry-17.4pkg:apk/chainguard/gitlab-container-registry-18.1pkg:apk/chainguard/gitlab-container-registry-18.2pkg:apk/chainguard/gitlab-container-registry-compat-17.4pkg:apk/chainguard/gitlab-container-registry-scripts-17.4pkg:apk/chainguard/gitlab-container-registry-scripts-18.1pkg:apk/chainguard/gitlab-container-registry-scripts-18.2pkg:apk/chainguard/gitlab-elasticsearch-indexer-17.4pkg:apk/chainguard/gitlab-elasticsearch-indexer-compat-17.4pkg:apk/chainguard/gitlab-exporter-17.4pkg:apk/chainguard/gitlab-exporter-18.1pkg:apk/chainguard/gitlab-exporter-18.2pkg:apk/chainguard/gitlab-exporter-scripts-17.4pkg:apk/chainguard/gitlab-exporter-scripts-18.1pkg:apk/chainguard/gitlab-exporter-scripts-18.2pkg:apk/chainguard/gitlab-geo-logcursor-scripts-17.4pkg:apk/chainguard/gitlab-gitaly-scripts-17.4pkg:apk/chainguard/gitlab-gitaly-scripts-18.1pkg:apk/chainguard/gitlab-gitaly-scripts-18.2pkg:apk/chainguard/gitlab-logger-17.4pkg:apk/chainguard/gitlab-logger-18.1pkg:apk/chainguard/gitlab-logger-18.2pkg:apk/chainguard/gitlab-logger-compat-17.4pkg:apk/chainguard/gitlab-logger-compat-18.1pkg:apk/chainguard/gitlab-logger-compat-18.2pkg:apk/chainguard/gitlab-mailroom-17.4pkg:apk/chainguard/gitlab-mailroom-scripts-17.4pkg:apk/chainguard/gitlab-pages-scripts-17.4pkg:apk/chainguard/gitlab-pages-scripts-18.1pkg:apk/chainguard/gitlab-pages-scripts-18.2pkg:apk/chainguard/gitlab-rails-scripts-17.4pkg:apk/chainguard/gitlab-rails-scripts-18.1pkg:apk/chainguard/gitlab-rails-scripts-18.2pkg:apk/chainguard/gitlab-shell-17.4pkg:apk/chainguard/gitlab-shell-18.1pkg:apk/chainguard/gitlab-shell-18.2pkg:apk/chainguard/gitlab-shell-scripts-17.4pkg:apk/chainguard/gitlab-shell-scripts-18.1pkg:apk/chainguard/gitlab-shell-scripts-18.2pkg:apk/chainguard/gitlab-shell-scripts-compat-17.4pkg:apk/chainguard/gitlab-shell-scripts-compat-18.1pkg:apk/chainguard/gitlab-shell-scripts-compat-18.2pkg:apk/chainguard/gitlab-sidekiq-ce-18.1pkg:apk/chainguard/gitlab-sidekiq-ce-18.2pkg:apk/chainguard/gitlab-sidekiq-scripts-17.4pkg:apk/chainguard/gitlab-toolbox-ce-18.1pkg:apk/chainguard/gitlab-toolbox-ce-18.2pkg:apk/chainguard/gitlab-toolbox-scripts-17.4pkg:apk/chainguard/gitlab-webservice-ce-18.1pkg:apk/chainguard/gitlab-webservice-ce-18.2pkg:apk/chainguard/gitlab-webservice-config-17.4pkg:apk/chainguard/gitlab-webservice-scripts-17.4pkg:apk/chainguard/gitlab-workhorse-scripts-17.4pkg:apk/chainguard/gitlab-workhorse-scripts-18.1pkg:apk/chainguard/gitlab-workhorse-scripts-18.2pkg:apk/chainguard/logstashpkg:apk/chainguard/logstash-compatpkg:apk/chainguard/logstash-env2yamlpkg:apk/chainguard/logstash-jre-bcfipspkg:apk/chainguard/logstash-jre-bcfips-compatpkg:apk/chainguard/logstash-jre-bcfips-env2yamlpkg:apk/chainguard/logstash-jre-bcfips-with-output-opensearchpkg:apk/chainguard/logstash-with-output-opensearchpkg:apk/chainguard/ruby3.2-pumapkg:apk/wolfi/gitaly-config-18.1pkg:apk/wolfi/gitaly-config-18.2pkg:apk/wolfi/gitlab-base-18.1pkg:apk/wolfi/gitlab-base-18.2pkg:apk/wolfi/gitlab-certificates-18.1pkg:apk/wolfi/gitlab-certificates-18.2pkg:apk/wolfi/gitlab-cfssl-self-sign-scripts-18.1pkg:apk/wolfi/gitlab-cfssl-self-sign-scripts-18.2pkg:apk/wolfi/gitlab-cng-18.1pkg:apk/wolfi/gitlab-cng-18.2pkg:apk/wolfi/gitlab-container-registry-18.1pkg:apk/wolfi/gitlab-container-registry-18.2pkg:apk/wolfi/gitlab-container-registry-scripts-18.1pkg:apk/wolfi/gitlab-container-registry-scripts-18.2pkg:apk/wolfi/gitlab-exporter-18.1pkg:apk/wolfi/gitlab-exporter-18.2pkg:apk/wolfi/gitlab-exporter-scripts-18.1pkg:apk/wolfi/gitlab-exporter-scripts-18.2pkg:apk/wolfi/gitlab-gitaly-scripts-18.1pkg:apk/wolfi/gitlab-gitaly-scripts-18.2pkg:apk/wolfi/gitlab-logger-18.1pkg:apk/wolfi/gitlab-logger-18.2pkg:apk/wolfi/gitlab-logger-compat-18.1pkg:apk/wolfi/gitlab-logger-compat-18.2pkg:apk/wolfi/gitlab-pages-scripts-18.1pkg:apk/wolfi/gitlab-pages-scripts-18.2pkg:apk/wolfi/gitlab-shell-18.1pkg:apk/wolfi/gitlab-shell-18.2pkg:apk/wolfi/gitlab-shell-scripts-18.1pkg:apk/wolfi/gitlab-shell-scripts-18.2pkg:apk/wolfi/gitlab-shell-scripts-compat-18.1pkg:apk/wolfi/gitlab-shell-scripts-compat-18.2pkg:apk/wolfi/logstashpkg:apk/wolfi/logstash-compatpkg:apk/wolfi/logstash-env2yamlpkg:apk/wolfi/logstash-with-output-opensearchpkg:apk/wolfi/ruby3.2-pumapkg:gem/pumapkg:rpm/opensuse/rubygem-puma&distro=openSUSE%20Leap%2015.5pkg:rpm/opensuse/rubygem-puma&distro=openSUSE%20Leap%2015.6pkg:rpm/opensuse/rubygem-puma&distro=openSUSE%20Tumbleweedpkg:rpm/suse/rubygem-puma&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP2pkg:rpm/suse/rubygem-puma&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP3pkg:rpm/suse/rubygem-puma&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP4pkg:rpm/suse/rubygem-puma&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP5pkg:rpm/suse/rubygem-puma&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP6pkg:rpm/suse/rubygem-puma&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP7
< 17.4.4-r0+ 125 more
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.3-r2
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.3-r2
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.3-r2
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.3-r2
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 17.4.4-r0
- (no CPE)range: < 18.1.3-r2
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 8.15.2-r1
- (no CPE)range: < 8.15.2-r1
- (no CPE)range: < 8.15.2-r1
- (no CPE)range: < 8.15.3-r0
- (no CPE)range: < 8.15.3-r0
- (no CPE)range: < 8.15.3-r0
- (no CPE)range: < 8.15.3-r0
- (no CPE)range: < 8.15.2-r1
- (no CPE)range: < 6.4.3-r0
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 18.1.2-r1
- (no CPE)range: < 18.2.1-r1
- (no CPE)range: < 8.15.2-r1
- (no CPE)range: < 8.15.2-r1
- (no CPE)range: < 8.15.2-r1
- (no CPE)range: < 8.15.2-r1
- (no CPE)range: < 6.4.3-r0
- (no CPE)range: < 5.6.9
- (no CPE)range: < 4.3.12-150000.3.15.1
- (no CPE)range: < 5.6.9-150600.18.3.1
- (no CPE)range: < 6.4.3-1.1
- (no CPE)range: < 4.3.12-150000.3.15.1
- (no CPE)range: < 4.3.12-150000.3.15.1
- (no CPE)range: < 4.3.12-150000.3.15.1
- (no CPE)range: < 4.3.12-150000.3.15.1
- (no CPE)range: < 5.6.9-150600.18.3.1
- (no CPE)range: < 5.6.9-150600.18.3.1
- puma/pumav5Range: >= 6.0.0, < 6.4.3
Patches
25 files changed · +111 −3
ext/puma_http11/org/jruby/puma/Http11.java+2 −0 modified@@ -99,6 +99,8 @@ public static void http_field(Ruby runtime, RubyHash req, ByteList buffer, int f int bite = b.get(i) & 0xFF; if(bite == '-') { b.set(i, (byte)'_'); + } else if(bite == '_') { + b.set(i, (byte)','); } else { b.set(i, (byte)Character.toUpperCase(bite)); }
lib/puma/const.rb+8 −0 modified@@ -281,6 +281,14 @@ module Const # header values can contain HTAB? ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze + # The keys of headers that should not be convert to underscore + # normalized versions. These headers are ignored at the request reading layer, + # but if we normalize them after reading, it's just confusing for the application. + UNMASKABLE_HEADERS = { + "HTTP_TRANSFER,ENCODING" => true, + "HTTP_CONTENT,LENGTH" => true, + } + # Banned keys of response header BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
lib/puma/request.rb+16 −3 modified@@ -495,6 +495,11 @@ def illegal_header_value?(header_value) # compatibility, we'll convert them back. This code is written to # avoid allocation in the common case (ie there are no headers # with `,` in their names), that's why it has the extra conditionals. + # + # @note If a normalized version of a `,` header already exists, we ignore + # the `,` version. This prevents clobbering headers managed by proxies + # but not by clients (Like X-Forwarded-For). + # # @param env [Hash] see Puma::Client#env, from request, modifies in place # @version 5.0.3 # @@ -503,23 +508,31 @@ def req_env_post_parse(env) to_add = nil env.each do |k,v| - if k.start_with?("HTTP_") && k.include?(",") && k != "HTTP_TRANSFER,ENCODING" + if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k) if to_delete to_delete << k else to_delete = [k] end + new_k = k.tr(",", "_") + if env.key?(new_k) + next + end + unless to_add to_add = {} end - to_add[k.tr(",", "_")] = v + to_add[new_k] = v end end - if to_delete + if to_delete # rubocop:disable Style/SafeNavigation to_delete.each { |k| env.delete(k) } + end + + if to_add env.merge! to_add end end
test/test_normalize.rb+57 −0 added@@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require_relative "helper" + +require "puma/request" + +class TestNormalize < Minitest::Test + parallelize_me! + + include Puma::Request + + def test_comma_headers + env = { + "HTTP_X_FORWARDED_FOR" => "1.1.1.1", + "HTTP_X_FORWARDED,FOR" => "2.2.2.2", + } + + req_env_post_parse env + + expected = { + "HTTP_X_FORWARDED_FOR" => "1.1.1.1", + } + + assert_equal expected, env + + # Test that the iteration order doesn't matter + + env = { + "HTTP_X_FORWARDED,FOR" => "2.2.2.2", + "HTTP_X_FORWARDED_FOR" => "1.1.1.1", + } + + req_env_post_parse env + + expected = { + "HTTP_X_FORWARDED_FOR" => "1.1.1.1", + } + + assert_equal expected, env + end + + def test_unmaskable_headers + env = { + "HTTP_CONTENT,LENGTH" => "100000", + "HTTP_TRANSFER,ENCODING" => "chunky" + } + + req_env_post_parse env + + expected = { + "HTTP_CONTENT,LENGTH" => "100000", + "HTTP_TRANSFER,ENCODING" => "chunky" + } + + assert_equal expected, env + end +end
test/test_request_invalid.rb+28 −0 modified@@ -215,4 +215,32 @@ def test_chunked_size_mismatch_2 assert_status data end + + def test_underscore_header_1 + hdrs = [ + "X-FORWARDED-FOR: 1.1.1.1", # proper + "X-FORWARDED-FOR: 2.2.2.2", # proper + "X_FORWARDED-FOR: 3.3.3.3", # invalid, contains underscore + "Content-Length: 5", + ].join "\r\n" + + response = send_http_and_read "#{GET_PREFIX}#{hdrs}\r\n\r\nHello\r\n\r\n" + + assert_includes response, "HTTP_X_FORWARDED_FOR = 1.1.1.1, 2.2.2.2" + refute_includes response, "3.3.3.3" + end + + def test_underscore_header_2 + hdrs = [ + "X_FORWARDED-FOR: 3.3.3.3", # invalid, contains underscore + "X-FORWARDED-FOR: 2.2.2.2", # proper + "X-FORWARDED-FOR: 1.1.1.1", # proper + "Content-Length: 5", + ].join "\r\n" + + response = send_http_and_read "#{GET_PREFIX}#{hdrs}\r\n\r\nHello\r\n\r\n" + + assert_includes response, "HTTP_X_FORWARDED_FOR = 2.2.2.2, 1.1.1.1" + refute_includes response, "3.3.3.3" + end end
5 files changed · +111 −3
ext/puma_http11/org/jruby/puma/Http11.java+2 −0 modified@@ -99,6 +99,8 @@ public static void http_field(Ruby runtime, RubyHash req, ByteList buffer, int f int bite = b.get(i) & 0xFF; if(bite == '-') { b.set(i, (byte)'_'); + } else if(bite == '_') { + b.set(i, (byte)','); } else { b.set(i, (byte)Character.toUpperCase(bite)); }
lib/puma/const.rb+8 −0 modified@@ -244,6 +244,14 @@ module Const # header values can contain HTAB? ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze + # The keys of headers that should not be convert to underscore + # normalized versions. These headers are ignored at the request reading layer, + # but if we normalize them after reading, it's just confusing for the application. + UNMASKABLE_HEADERS = { + "HTTP_TRANSFER,ENCODING" => true, + "HTTP_CONTENT,LENGTH" => true, + } + # Banned keys of response header BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
lib/puma/request.rb+16 −3 modified@@ -318,6 +318,11 @@ def illegal_header_value?(header_value) # compatibility, we'll convert them back. This code is written to # avoid allocation in the common case (ie there are no headers # with `,` in their names), that's why it has the extra conditionals. + # + # @note If a normalized version of a `,` header already exists, we ignore + # the `,` version. This prevents clobbering headers managed by proxies + # but not by clients (Like X-Forwarded-For). + # # @param env [Hash] see Puma::Client#env, from request, modifies in place # @version 5.0.3 # @@ -326,23 +331,31 @@ def req_env_post_parse(env) to_add = nil env.each do |k,v| - if k.start_with?("HTTP_") and k.include?(",") and k != "HTTP_TRANSFER,ENCODING" + if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k) if to_delete to_delete << k else to_delete = [k] end + new_k = k.tr(",", "_") + if env.key?(new_k) + next + end + unless to_add to_add = {} end - to_add[k.tr(",", "_")] = v + to_add[new_k] = v end end - if to_delete + if to_delete # rubocop:disable Style/SafeNavigation to_delete.each { |k| env.delete(k) } + end + + if to_add env.merge! to_add end end
test/test_normalize.rb+57 −0 added@@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require_relative "helper" + +require "puma/request" + +class TestNormalize < Minitest::Test + parallelize_me! + + include Puma::Request + + def test_comma_headers + env = { + "HTTP_X_FORWARDED_FOR" => "1.1.1.1", + "HTTP_X_FORWARDED,FOR" => "2.2.2.2", + } + + req_env_post_parse env + + expected = { + "HTTP_X_FORWARDED_FOR" => "1.1.1.1", + } + + assert_equal expected, env + + # Test that the iteration order doesn't matter + + env = { + "HTTP_X_FORWARDED,FOR" => "2.2.2.2", + "HTTP_X_FORWARDED_FOR" => "1.1.1.1", + } + + req_env_post_parse env + + expected = { + "HTTP_X_FORWARDED_FOR" => "1.1.1.1", + } + + assert_equal expected, env + end + + def test_unmaskable_headers + env = { + "HTTP_CONTENT,LENGTH" => "100000", + "HTTP_TRANSFER,ENCODING" => "chunky" + } + + req_env_post_parse env + + expected = { + "HTTP_CONTENT,LENGTH" => "100000", + "HTTP_TRANSFER,ENCODING" => "chunky" + } + + assert_equal expected, env + end +end
test/test_request_invalid.rb+28 −0 modified@@ -216,4 +216,32 @@ def test_chunked_size_mismatch_2 assert_status data end + + def test_underscore_header_1 + hdrs = [ + "X-FORWARDED-FOR: 1.1.1.1", # proper + "X-FORWARDED-FOR: 2.2.2.2", # proper + "X_FORWARDED-FOR: 3.3.3.3", # invalid, contains underscore + "Content-Length: 5", + ].join "\r\n" + + response = send_http_and_read "#{GET_PREFIX}#{hdrs}\r\n\r\nHello\r\n\r\n" + + assert_includes response, "HTTP_X_FORWARDED_FOR = 1.1.1.1, 2.2.2.2" + refute_includes response, "3.3.3.3" + end + + def test_underscore_header_2 + hdrs = [ + "X_FORWARDED-FOR: 3.3.3.3", # invalid, contains underscore + "X-FORWARDED-FOR: 2.2.2.2", # proper + "X-FORWARDED-FOR: 1.1.1.1", # proper + "Content-Length: 5", + ].join "\r\n" + + response = send_http_and_read "#{GET_PREFIX}#{hdrs}\r\n\r\nHello\r\n\r\n" + + assert_includes response, "HTTP_X_FORWARDED_FOR = 2.2.2.2, 1.1.1.1" + refute_includes response, "3.3.3.3" + 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
8- github.com/advisories/GHSA-9hf4-67fc-4vf4ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-45614ghsaADVISORY
- github.com/puma/puma/commit/cac3fd18cf29ed43719ff5d52d9cfec215f0a043ghsaWEB
- github.com/puma/puma/commit/f196b23be24712fb8fb16051cc124798cc84f70eghsaWEB
- github.com/puma/puma/security/advisories/GHSA-9hf4-67fc-4vf4ghsax_refsource_CONFIRMWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/puma/CVE-2024-45614.ymlghsaWEB
- lists.debian.org/debian-lts-announce/2024/11/msg00004.htmlghsaWEB
- nginx.org/en/docs/http/ngx_http_core_module.htmlghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.