From c73d96f831756789e8715d1762bf67aa68836342 Mon Sep 17 00:00:00 2001 From: Ian Gordon Date: Tue, 28 Apr 2026 11:45:57 -0400 Subject: [PATCH] =?UTF-8?q?Reject=20control=20frames=20with=20payload=20>1?= =?UTF-8?q?25=20bytes=20per=20RFC=206455=20=C2=A75.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Control frames (ping/pong/close) MUST have payload length ≤ 125 bytes. Without this guard, a peer could send a 1 MB ping and have it echoed back verbatim as a pong by WSHandler.makeResponseFrames. Closes TVT-306 Co-Authored-By: Claude Opus 4.7 --- .../Sources/WebSocket/WSFrameValidator.swift | 10 ++++++++ .../WebSocket/WSFrameValidatorTests.swift | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/FlyingFox/Sources/WebSocket/WSFrameValidator.swift b/FlyingFox/Sources/WebSocket/WSFrameValidator.swift index 0437351f..9df7b190 100644 --- a/FlyingFox/Sources/WebSocket/WSFrameValidator.swift +++ b/FlyingFox/Sources/WebSocket/WSFrameValidator.swift @@ -45,6 +45,16 @@ struct WSFrameValidator: Sendable { @Sendable func validateFrame(_ frame: WSFrame) throws -> WSFrame? { + switch frame.opcode { + case .ping, .pong, .close: + // RFC 6455 §5.5: control frame payload MUST be ≤ 125 bytes. + guard frame.payload.count <= 125 else { + throw Error("Control frame payload exceeds 125 bytes") + } + default: + break + } + if frame.opcode == .continuation { try appendContinuation(frame) guard let last = last, frame.fin else { diff --git a/FlyingFox/Tests/WebSocket/WSFrameValidatorTests.swift b/FlyingFox/Tests/WebSocket/WSFrameValidatorTests.swift index 21d5ba07..dc5dd9c5 100644 --- a/FlyingFox/Tests/WebSocket/WSFrameValidatorTests.swift +++ b/FlyingFox/Tests/WebSocket/WSFrameValidatorTests.swift @@ -80,6 +80,31 @@ struct WSFrameValidatorTests { } } + @Test + func controlFrames_throwError_whenPayloadExceeds125Bytes() async { + // RFC 6455 §5.5: control frames MUST have payload length ≤ 125 bytes. + let oversized = Data(repeating: 0x41, count: 126) + await #expect(throws: WSFrameValidator.Error.self) { + try await WSFrameValidator.validate([.make(opcode: .ping, payload: oversized)]).collectAll() + } + await #expect(throws: WSFrameValidator.Error.self) { + try await WSFrameValidator.validate([.make(opcode: .pong, payload: oversized)]).collectAll() + } + await #expect(throws: WSFrameValidator.Error.self) { + try await WSFrameValidator.validate([.make(opcode: .close, payload: oversized)]).collectAll() + } + } + + @Test + func controlFrames_areAccepted_whenPayloadIsAtMost125Bytes() async throws { + let maxPayload = Data(repeating: 0x41, count: 125) + let ping = WSFrame.make(opcode: .ping, payload: maxPayload) + let emptyPing = WSFrame.make(opcode: .ping) + #expect( + try await WSFrameValidator.validate([ping, emptyPing]).collectAll() == [ping, emptyPing] + ) + } + @Test func controlFrames_ThrowError_WhenNotFin() async { await #expect(throws: WSFrameValidator.Error.self) {