VYPR
High severity7.5NVD Advisory· Published Jun 8, 2026· Updated Jun 8, 2026

Puma PROXY Protocol v1 Parser Allows Remote Memory Exhaustion

CVE-2026-47736

Description

Puma PROXY protocol v1 parsing allows remote memory exhaustion via unbounded buffer growth on unauthenticated TCP connections.

AI Insight

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

Puma PROXY protocol v1 parsing allows remote memory exhaustion via unbounded buffer growth on unauthenticated TCP connections.

Vulnerability

When PROXY protocol v1 support is enabled in Puma servers configured with set_remote_address proxy_protocol: :v1, the server reads incoming bytes into an internal buffer and waits for \r\n to identify a PROXY v1 line. If an attacker sends continuous bytes without CRLF, Puma appends these to a pre-parse buffer, causing unbounded in-process memory growth and increased CPU usage from buffer scanning. This vulnerability affects Puma versions 5.5.0 up to, but not including, 7.2.1 and versions 8.0.0 up to, but not including, 8.0.2 [2].

Exploitation

An unauthenticated attacker can exploit this vulnerability by opening a single TCP connection to a Puma server configured with PROXY protocol v1 support and continuously sending data without the expected \r\n sequence. This requires network access to the Puma listener, but no other privileges or user interaction are needed [3].

Impact

Successful exploitation can lead to significant memory growth within the Puma process, potentially causing the process or its container to be Out Of Memory (OOM) killed. This results in degraded availability of the application [2, 3].

Mitigation

Puma versions 7.2.1 and 8.0.2 contain fixes for this vulnerability [1, 2]. Users should upgrade to these versions or later. If upgrading is not immediately possible, PROXY protocol v1 parsing can be disabled by removing or commenting out the set_remote_address proxy_protocol: :v1 configuration. Additionally, restricting direct network access to Puma listeners and only allowing trusted load balancers or reverse proxies to connect can mitigate the risk [1].

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

Affected products

1
  • Puma/pumallm-create
    Range: up to 7.2.1, up to 8.0.2

Patches

2
ebe9db3929ab

7.2.1 backport (#3947)

https://github.com/puma/pumaNate BerkopecMay 26, 2026Fixed in 7.2.1via ghsa-release-walk
3 files changed · +100 12
  • lib/puma/client.rb+27 11 modified
    @@ -157,7 +157,7 @@ def reset
           @parser.reset
           @io_buffer.reset
           @read_header = true
    -      @read_proxy = !!@expect_proxy_proto
    +      @read_proxy = !!@expect_proxy_proto && @requests_served.zero?
           @env = @proto_env.dup
           @parsed_bytes = 0
           @ready = false
    @@ -211,20 +211,36 @@ def tempfile_close
         def try_to_parse_proxy_protocol
           if @read_proxy
             if @expect_proxy_proto == :v1
    -          if @buffer.include? "\r\n"
    -            if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
    -              if md[1]
    -                @peerip = md[1].split(" ")[0]
    +          crlf_index = @buffer.index "\r\n"
    +
    +          unless crlf_index
    +            if "PROXY ".start_with? @buffer
    +              return false
    +            elsif @buffer.start_with? "PROXY "
    +              if @buffer.bytesize >= PROXY_PROTOCOL_V1_MAX_LENGTH
    +                raise ConnectionError, "PROXY protocol v1 line is too long"
                   end
    -              @buffer = md.post_match
    +              return false
                 end
    -            # if the buffer has a \r\n but doesn't have a PROXY protocol
    -            # request, this is just HTTP from a non-PROXY client; move on
    +
                 @read_proxy = false
    -            return @buffer.size > 0
    -          else
    -            return false
    +            return true
    +          end
    +
    +          if @buffer.start_with?("PROXY ") && crlf_index + 2 > PROXY_PROTOCOL_V1_MAX_LENGTH
    +            raise ConnectionError, "PROXY protocol v1 line is too long"
    +          end
    +
    +          if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
    +            if md[1]
    +              @peerip = md[1].split(" ")[0]
    +            end
    +            @buffer = md.post_match
               end
    +          # if the buffer has a \r\n but doesn't have a PROXY protocol
    +          # request, this is just HTTP from a non-PROXY client; move on
    +          @read_proxy = false
    +          return @buffer.size > 0
             end
           end
           true
    
  • lib/puma/const.rb+2 1 modified
    @@ -291,7 +291,8 @@ module Const
         # Banned keys of response header
         BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
     
    -    PROXY_PROTOCOL_V1_REGEX = /^PROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
    +    PROXY_PROTOCOL_V1_REGEX = /\APROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
    +    PROXY_PROTOCOL_V1_MAX_LENGTH = 107
     
         # All constants are prefixed with `PIPE_` to avoid name collisions.
         module PipeRequest
    
  • test/test_puma_server.rb+71 0 modified
    @@ -1623,6 +1623,77 @@ def test_proxy_protocol
         assert_equal 'fd00::1', remote_addr
       end
     
    +  def test_proxy_protocol_rejects_line_without_crlf_at_max_length
    +    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do
    +      [200, {}, ["Hello"]]
    +    end
    +
    +    socket = new_socket
    +    socket << "PROXY #{'A' * (Puma::Const::PROXY_PROTOCOL_V1_MAX_LENGTH - "PROXY ".bytesize)}"
    +
    +    assert_raises EOFError, Errno::ECONNABORTED, Errno::ECONNRESET do
    +      socket.read_response(timeout: 1)
    +    end
    +  end
    +
    +  def test_proxy_protocol_allows_non_proxy_requests_over_max_length
    +    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do
    +      [200, {}, ["Hello"]]
    +    end
    +
    +    response = send_http_read_response "GET /#{'a' * Puma::Const::PROXY_PROTOCOL_V1_MAX_LENGTH} HTTP/1.0\r\n\r\n"
    +
    +    assert_equal "HTTP/1.0 200 OK", response.status
    +    assert_equal "Hello", response.body
    +  end
    +
    +  def test_proxy_protocol_ignores_embedded_proxy_line_in_http_body
    +    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do |env|
    +      body = env['rack.input'].read
    +      [200, {'Content-Length' => body.bytesize.to_s}, [body]]
    +    end
    +
    +    body = "prefix\nPROXY TCP4 1.1.1.1 2.2.2.2 3 4\r\nsuffix"
    +    response = send_http_read_response "POST / HTTP/1.1\r\nHost: test.com\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}"
    +
    +    assert_equal "HTTP/1.1 200 OK", response.status
    +    assert_equal body, response.body
    +  end
    +
    +  def test_proxy_protocol_only_parses_first_request_on_connection
    +    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do |env|
    +      [200, {}, [env["REMOTE_ADDR"]]]
    +    end
    +
    +    socket = send_proxy_v1_http("GET /one HTTP/1.1\r\nHost: test.com\r\n\r\n", "1.2.3.4")
    +
    +    response = socket.read_response
    +    assert_equal "HTTP/1.1 200 OK", response.status
    +    assert_equal "1.2.3.4", response.body
    +
    +    socket << "PROXY TCP4 127.0.0.1 127.0.0.1 10000 80\r\nGET /two HTTP/1.1\r\nHost: test.com\r\nConnection: close\r\n\r\n"
    +
    +    assert_match %r{\AHTTP/1\.[01] 400 Bad Request\z}, socket.read_response.status
    +  end
    +
    +  def test_proxy_protocol_allows_proxy_http_method_on_keep_alive
    +    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1, supported_http_methods: :any) do |env|
    +      [200, {}, [env["REMOTE_ADDR"]]]
    +    end
    +
    +    socket = send_proxy_v1_http("GET /one HTTP/1.1\r\nHost: test.com\r\n\r\n", "1.2.3.4")
    +
    +    response = socket.read_response
    +    assert_equal "HTTP/1.1 200 OK", response.status
    +    assert_equal "1.2.3.4", response.body
    +
    +    socket << "PROXY /two HTTP/1.1\r\nHost: test.com\r\nConnection: close\r\n\r\n"
    +
    +    response = socket.read_response
    +    assert_equal "HTTP/1.1 200 OK", response.status
    +    assert_equal "1.2.3.4", response.body
    +  end
    +
       # 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 = {})
    
439c6136d9c2

8.0.2 backport (#3944)

https://github.com/puma/pumaNate BerkopecMay 26, 2026Fixed in 8.0.2via ghsa-release-walk
3 files changed · +100 12
  • lib/puma/client.rb+27 11 modified
    @@ -163,7 +163,7 @@ def reset
           @parser.reset
           @io_buffer.reset
           @read_header = true
    -      @read_proxy = !!@expect_proxy_proto
    +      @read_proxy = !!@expect_proxy_proto && @requests_served.zero?
           @env = @proto_env.dup
           @parsed_bytes = 0
           @ready = false
    @@ -213,20 +213,36 @@ def tempfile_close
         def try_to_parse_proxy_protocol
           if @read_proxy
             if @expect_proxy_proto == :v1
    -          if @buffer.include? "\r\n"
    -            if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
    -              if md[1]
    -                @peerip = md[1].split(" ")[0]
    +          crlf_index = @buffer.index "\r\n"
    +
    +          unless crlf_index
    +            if "PROXY ".start_with? @buffer
    +              return false
    +            elsif @buffer.start_with? "PROXY "
    +              if @buffer.bytesize >= PROXY_PROTOCOL_V1_MAX_LENGTH
    +                raise ConnectionError, "PROXY protocol v1 line is too long"
                   end
    -              @buffer = md.post_match
    +              return false
                 end
    -            # if the buffer has a \r\n but doesn't have a PROXY protocol
    -            # request, this is just HTTP from a non-PROXY client; move on
    +
                 @read_proxy = false
    -            return @buffer.size > 0
    -          else
    -            return false
    +            return true
    +          end
    +
    +          if @buffer.start_with?("PROXY ") && crlf_index + 2 > PROXY_PROTOCOL_V1_MAX_LENGTH
    +            raise ConnectionError, "PROXY protocol v1 line is too long"
    +          end
    +
    +          if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
    +            if md[1]
    +              @peerip = md[1].split(" ")[0]
    +            end
    +            @buffer = md.post_match
               end
    +          # if the buffer has a \r\n but doesn't have a PROXY protocol
    +          # request, this is just HTTP from a non-PROXY client; move on
    +          @read_proxy = false
    +          return @buffer.size > 0
             end
           end
           true
    
  • lib/puma/const.rb+2 1 modified
    @@ -291,7 +291,8 @@ module Const
         # Banned keys of response header
         BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
     
    -    PROXY_PROTOCOL_V1_REGEX = /^PROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
    +    PROXY_PROTOCOL_V1_REGEX = /\APROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
    +    PROXY_PROTOCOL_V1_MAX_LENGTH = 107
     
         # All constants are prefixed with `PIPE_` to avoid name collisions.
         module PipeRequest
    
  • test/test_puma_server.rb+71 0 modified
    @@ -1528,6 +1528,77 @@ def test_proxy_protocol
         assert_equal 'fd00::1', remote_addr
       end
     
    +  def test_proxy_protocol_rejects_line_without_crlf_at_max_length
    +    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do
    +      [200, {}, ["Hello"]]
    +    end
    +
    +    socket = new_socket
    +    socket << "PROXY #{'A' * (Puma::Const::PROXY_PROTOCOL_V1_MAX_LENGTH - "PROXY ".bytesize)}"
    +
    +    assert_raises EOFError, Errno::ECONNABORTED, Errno::ECONNRESET do
    +      socket.read_response(timeout: 1)
    +    end
    +  end
    +
    +  def test_proxy_protocol_allows_non_proxy_requests_over_max_length
    +    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do
    +      [200, {}, ["Hello"]]
    +    end
    +
    +    response = send_http_read_response "GET /#{'a' * Puma::Const::PROXY_PROTOCOL_V1_MAX_LENGTH} HTTP/1.0\r\n\r\n"
    +
    +    assert_equal "HTTP/1.0 200 OK", response.status
    +    assert_equal "Hello", response.body
    +  end
    +
    +  def test_proxy_protocol_ignores_embedded_proxy_line_in_http_body
    +    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do |env|
    +      body = env['rack.input'].read
    +      [200, {'Content-Length' => body.bytesize.to_s}, [body]]
    +    end
    +
    +    body = "prefix\nPROXY TCP4 1.1.1.1 2.2.2.2 3 4\r\nsuffix"
    +    response = send_http_read_response "POST / HTTP/1.1\r\nHost: test.com\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}"
    +
    +    assert_equal "HTTP/1.1 200 OK", response.status
    +    assert_equal body, response.body
    +  end
    +
    +  def test_proxy_protocol_only_parses_first_request_on_connection
    +    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do |env|
    +      [200, {}, [env["REMOTE_ADDR"]]]
    +    end
    +
    +    socket = send_proxy_v1_http("GET /one HTTP/1.1\r\nHost: test.com\r\n\r\n", "1.2.3.4")
    +
    +    response = socket.read_response
    +    assert_equal "HTTP/1.1 200 OK", response.status
    +    assert_equal "1.2.3.4", response.body
    +
    +    socket << "PROXY TCP4 127.0.0.1 127.0.0.1 10000 80\r\nGET /two HTTP/1.1\r\nHost: test.com\r\nConnection: close\r\n\r\n"
    +
    +    assert_match %r{\AHTTP/1\.[01] 400 Bad Request\z}, socket.read_response.status
    +  end
    +
    +  def test_proxy_protocol_allows_proxy_http_method_on_keep_alive
    +    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1, supported_http_methods: :any) do |env|
    +      [200, {}, [env["REMOTE_ADDR"]]]
    +    end
    +
    +    socket = send_proxy_v1_http("GET /one HTTP/1.1\r\nHost: test.com\r\n\r\n", "1.2.3.4")
    +
    +    response = socket.read_response
    +    assert_equal "HTTP/1.1 200 OK", response.status
    +    assert_equal "1.2.3.4", response.body
    +
    +    socket << "PROXY /two HTTP/1.1\r\nHost: test.com\r\nConnection: close\r\n\r\n"
    +
    +    response = socket.read_response
    +    assert_equal "HTTP/1.1 200 OK", response.status
    +    assert_equal "1.2.3.4", response.body
    +  end
    +
       # 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 = {})
    

Vulnerability mechanics

Synthesis attempt was rejected by the grounding validator. Re-run pending.

References

4

News mentions

0

No linked articles in our index yet.