Repository navigation
Bound incoming WebSocket frames and validate close payloads - #248
Merged
Merged
Conversation
WSFrameEncoder.decodeFrame read whatever payload length the frame header declared, up to Int.max, so a client could declare a 2^40 byte frame and feed bytes until the server ran out of memory. Any size check a handler makes on the assembled message comes after that. Check the declared length against a maximum right after the length and mask are decoded, before any payload is read. The limit is HTTPServer.Configuration.webSocketMaxFrameSize, 16 MiB by default, and HTTPConnection passes it to the client frame decoder. An oversized frame fails the stream with a WSFrameEncoder.Error carrying close code 1009; WSFrameEncoder.Error now names the close code a handler should send, so MessageFrameWSHandler answers with 1009 (Message Too Big) instead of a generic 1002. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T8cahEd4GfsHQ89sNrmP4p
WSFrameValidator checked only that a close frame's payload fits in 125 bytes. RFC 6455 §5.5.1 also requires a non-empty payload to begin with a 2 byte status code, §7.4 defines which codes an endpoint may send, and the reason that follows must be UTF-8. Reject a 1 byte payload, a status code outside 1000-1003, 1007-1014 and 3000-4999 (1005, 1006 and 1015 are reserved for local use; 1004 and 1016-2999 for future revisions), and a reason that is not valid UTF-8. Each closes the connection with 1002 like the validator's other errors. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T8cahEd4GfsHQ89sNrmP4p
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #248 +/- ##
==========================================
+ Coverage 93.75% 93.79% +0.04%
==========================================
Files 72 72
Lines 3793 3820 +27
==========================================
+ Hits 3556 3583 +27
Misses 237 237 ☔ View full report in Codecov by Harness. |
swhitty
approved these changes
Oct 12, 2026
swhitty
left a comment
Owner
There was a problem hiding this comment.
nice! I have empathy for that little ARM board that was taken down due to this issue! thanks
Contributor
Author
|
Claude may have exaggerated, lol. The test server that crashed was running on a Ryzen AI Max. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
WSFrameEncoder.decodeFramereads whatever payload length the frame header declares, up toInt.max:decodeLengthMaskaccepts a 2^40 byte length anddecodePayloadthen callsnextBuffer(count:)for it. A client can declare such a frame and stream bytes until the server runs out of memory, and any message size check a handler makes comes after the payload has been assembled. We hit this with a server on a small ARM board where a single misbehaving client takes the process down.Change
WSFrameEncoder.decodeFrame(from:maxPayloadSize:),decodeClientFrame(from:maxPayloadSize:)andAsyncThrowingStream.decodingClientFrames(from:maxPayloadSize:)take it; the existing calls keep their behaviour through a default of.max.HTTPServer.Configuration.webSocketMaxFrameSize, 16 MiB by default, andHTTPConnectionpasses it to the client frame decoder.WSFrameEncoder.ErrorgainscloseCode(default.protocolError), so an oversized frame is answered with 1009 (Message Too Big) rather than a generic 1002.AsyncStream.protocolFramessends that code; every other error still closes with 1002 as before.WSFrameValidatorapplies the remaining close frame rules from RFC 6455 §5.5.1 and §7.4: a 1 byte payload, a status code outside 1000–1003, 1007–1014 and 3000–4999 (1005, 1006 and 1015 are reserved for local use; 1004 and 1016–2999 for future revisions), or a reason that is not UTF-8 each close with 1002. It already enforced the 125 byte limit and, since Reject control frames with payload >125 bytes per RFC 6455 §5.5 #214/cc5aea8, unmasked client frames are rejected at the decoder.Testing
New tests: the decoder rejects a frame declaring 256 bytes against a 255 byte limit before reading any payload, and accepts one exactly at the limit; the client frame stream rejects an oversized frame;
protocolFramescloses with the error's code; the validator's close frame rules; anHTTPConnectionwith a 1 KiB limit answers a 1025 byte frame with.close(code: .messageTooBig); and anHTTPServerconfigured withwebSocketMaxFrameSize: 16echoes a 16 byte message and closes with 1009 on a 17 byte one.swift teston Linux (Swift 6.4): 310 tests, the same 3 issues as onmainwithout this change (HTTPDecoderTests.standardizedPathandnaughtyPath_IsParsed). Not run on Darwin locally.A follow-up makes the frames↔messages path in
MessageFrameWSHandlerpull-based, so a client that stops reading cannot grow the output buffer either.🤖 Generated with Claude Code
https://claude.ai/code/session_01T8cahEd4GfsHQ89sNrmP4p