Summary
WEBrick contains two independent weaknesses in HTTP/1.1 request parsing
that create HTTP Request Smuggling conditions when deployed behind a
reverse proxy (Nginx, HAProxy, AWS ALB) with connection pooling.
Both bugs are confirmed directly in WEBrick via raw TCP no proxy
required to reproduce the parsing behavior.
This report was originally submitted via HackerOne with PoC and redirected here by @hsbt.
Bug 1 TE.TE Desync: Multiple Transfer-Encoding Headers
File: lib/webrick/httputils.rb:155-159 / lib/webrick/httprequest.rb:550-557
CWE: CWE-444
When a request contains multiple Transfer-Encoding headers, WEBrick
joins their values using ", " via SplitHeader#join, producing
"chunked, identity". This fails the regex /\Achunked\z/io, causing
WEBrick to return 501 Not Implemented instead of the RFC-required
400 Bad Request.
The unread body bytes remain in the socket buffer and are processed
as the next request under a keep-alive connection.
Minimal reproduction (direct TCP, no proxy):
import socket
def raw(payload):
s = socket.socket()
s.connect(("127.0.0.1", 18080))
s.sendall(payload)
r = b""
s.settimeout(2)
try:
while True:
c = s.recv(4096)
if not c: break
r += c
except: pass
s.close()
return r.decode(errors="replace")
# Single TE header -> 200 OK
raw(b"POST /submit HTTP/1.1\r\nHost: localhost\r\n"
b"Transfer-Encoding: chunked\r\nConnection: close\r\n\r\n"
b"5\r\nhello\r\n0\r\n\r\n")
# Two TE headers -> 501 Not Implemented
raw(b"POST /submit HTTP/1.1\r\nHost: localhost\r\n"
b"Transfer-Encoding: chunked\r\n"
b"Transfer-Encoding: identity\r\n"
b"Connection: close\r\n\r\n"
b"5\r\nhello\r\n0\r\n\r\n")
Observed output (WEBrick 1.9.2 / Ruby 4.0.1):
Single TE: HTTP/1.1 200 OK
Two TE: HTTP/1.1 501 Not Implemented
body: "Transfer-Encoding: chunked, identity."
The 501 response body exposes the joined value, confirming
SplitHeader#join in httputils.rb:156 as the root cause.
Bug 2 - Obs-fold Desync: Obsolete Line Folding in Transfer-Encoding
File: lib/webrick/httputils.rb:181-188
CWE: CWE-444
WEBrick's parse_header merges obs-fold continuation lines into the
previous header value. A request containing:
Transfer-Encoding: chunked\r\n
\tidentity\r\n
Is parsed by WEBrick as Transfer-Encoding: chunked identity.
Frontend proxies that do not support obs-fold (HAProxy < 2.0.6, standard
Nginx) see only Transfer-Encoding: chunked and process the body
accordingly - creating a parsing asymmetry that is the foundation of
TE.TE smuggling. This is the same mechanism documented in CVE-2019-18277.
RFC 7230 §3.2.6 states servers MUST either reject obs-fold in request
headers or replace it with SP octets. WEBrick applies the second option
but does not validate the resulting TE value, leaving the desync surface open.
Affected Versions
Confirmed on WEBrick 1.9.2 / Ruby 4.0.1. Likely affects earlier versions.
Recommended Fixes
Bug 1 - Reject multiple Transfer-Encoding headers
# lib/webrick/httprequest.rb - inside read_header
if (te = @header['transfer-encoding']) && te.length > 1
raise HTTPStatus::BadRequest, "multiple transfer-encoding headers"
end
Bug 2 - Reject obs-fold in request headers
# lib/webrick/httputils.rb:181-188
when /^[ \t]+([^\r\n\0]*?)\r\n/om
raise HTTPStatus::BadRequest,
"obs-fold in request header is not allowed (RFC 7230 §3.2.6)"
References
Full technical details and end-to-end PoC available upon request.
Summary
WEBrick contains two independent weaknesses in HTTP/1.1 request parsing
that create HTTP Request Smuggling conditions when deployed behind a
reverse proxy (Nginx, HAProxy, AWS ALB) with connection pooling.
Both bugs are confirmed directly in WEBrick via raw TCP no proxy
required to reproduce the parsing behavior.
Bug 1 TE.TE Desync: Multiple Transfer-Encoding Headers
File:
lib/webrick/httputils.rb:155-159/lib/webrick/httprequest.rb:550-557CWE: CWE-444
When a request contains multiple
Transfer-Encodingheaders, WEBrickjoins their values using
", "viaSplitHeader#join, producing"chunked, identity". This fails the regex/\Achunked\z/io, causingWEBrick to return
501 Not Implementedinstead of the RFC-required400 Bad Request.The unread body bytes remain in the socket buffer and are processed
as the next request under a keep-alive connection.
Minimal reproduction (direct TCP, no proxy):
Observed output (WEBrick 1.9.2 / Ruby 4.0.1):
The 501 response body exposes the joined value, confirming
SplitHeader#joininhttputils.rb:156as the root cause.Bug 2 - Obs-fold Desync: Obsolete Line Folding in Transfer-Encoding
File:
lib/webrick/httputils.rb:181-188CWE: CWE-444
WEBrick's
parse_headermerges obs-fold continuation lines into theprevious header value. A request containing:
Is parsed by WEBrick as
Transfer-Encoding: chunked identity.Frontend proxies that do not support obs-fold (HAProxy < 2.0.6, standard
Nginx) see only
Transfer-Encoding: chunkedand process the bodyaccordingly - creating a parsing asymmetry that is the foundation of
TE.TE smuggling. This is the same mechanism documented in CVE-2019-18277.
RFC 7230 §3.2.6 states servers MUST either reject obs-fold in request
headers or replace it with SP octets. WEBrick applies the second option
but does not validate the resulting TE value, leaving the desync surface open.
Affected Versions
Confirmed on WEBrick 1.9.2 / Ruby 4.0.1. Likely affects earlier versions.
Recommended Fixes
Bug 1 - Reject multiple Transfer-Encoding headers
Bug 2 - Reject obs-fold in request headers
References
Full technical details and end-to-end PoC available upon request.