bugfix: client: bound the handshake response header read - #7
shreemaan-abhishek wants to merge 1 commit into
Conversation
The handshake response header block was read with an unbounded
receiveuntil("\r\n\r\n"), so a malicious or broken server could stream
bytes forever without ever sending the terminator and the cosocket
would buffer all of it, exhausting the nginx worker's memory and
taking down every request it is handling.
Read at most max_header_len bytes (8192 by default, mirroring nginx's
own header buffer) and refuse anything larger. Set max_header_len = 0
to restore the unbounded read.
The same cap covers the CONNECT response read on the wss-over-proxy
path, which had the same unbounded read.
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Essentials Run ID: 📒 Files selected for processing (3)
Included review availability: 4 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 5 reviews per hour. 📝 WalkthroughWalkthroughThe WebSocket client now limits handshake response headers to 8192 bytes by default. The ChangesHandshake header limits
Priority: ⬆️ High Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant Client
participant Proxy
participant WebSocketServer
Client->>Proxy: Send CONNECT request
Proxy-->>Client: Return CONNECT response headers
Client->>WebSocketServer: Send WebSocket handshake
WebSocketServer-->>Client: Return handshake response headers
Client->>Client: Apply max_header_len while reading headers
Suggested reviewers: Merge Risk: 🔵 Low · up to The client now bounds handshake headers by default, reducing memory-exhaustion exposure. Proxy CONNECT handling is described as using the same limit, but its path lacks a dedicated test, leaving a low bounded regression risk. 🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
Full details: E2e Test Quality ReviewExplanation The new suite uses real nginx-to-cosocket flows and covers default, unlimited, invalid, oversized, and unterminated headers. However, it violates the blocking error-handling criterion: several added cases call Resolution Check and handle the return values from every added
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
What
The WebSocket handshake response header block was read with an unbounded
sock:receiveuntil("\r\n\r\n"), with a standing-- FIXME: check for too big response headersnext to it.A malicious or broken server can stream bytes forever without ever sending the
\r\n\r\nterminator, and the cosocket buffers all of it. That exhausts thenginx worker's memory and takes down every request that worker is handling, not
just the WebSocket one. The read timeout does not help: nginx resets the read
timer on each read event, so a steady drip keeps the read alive indefinitely.
CWE-770, remote DoS. It needs a hostile or compromised server, or a MITM on a
plaintext
ws://connection.Note the asymmetry:
server.luahas no equivalent hole, because nginx capsrequest headers itself via
large_client_header_buffers. Only the client sidewas unbounded.
How
recv_header()reads at mostmax_header_lenbytes by asking the iterator forone byte past the limit: a header block that fits comes back whole, while an
oversized one, or a peer that never terminates the block at all, comes back at
the limit and is refused. Peak buffering is bounded at
max_header_len + 1bytes.
The same helper covers the
CONNECTresponse read on the wss-over-proxy path,which had the identical unbounded read.
Behavior change
max_header_lendefaults to8192, mirroring nginx's own 8k header buffer, sothe protection is on without opting in. A server sending a larger handshake
header block is now refused with:
Set
max_header_len = 0inclient:new()to restore the previous unboundedread.
Tests
t/max_header_len.tcovers the default limit,max_header_len = 0, a smallerexplicit limit, a header block that is never terminated,
new()rejecting a badvalue, and a real handshake still succeeding under the default.
Notes
Upstream
openresty/lua-resty-websocket#94proposes amax_header_lenoptionfor the same FIXME. The option name and semantics here match it, so a later
upstream sync is a no-op rather than a conflict. The default differs: upstream
leaves it opt-in at
0, which keeps the DoS reachable out of the box.Summary by CodeRabbit
New Features
max_header_lento0to allow unlimited header sizes, or use a smaller non-negative limit.Documentation
max_header_lenbehavior and defaults.Bug Fixes