Skip to content

Commit 12757c5

Browse files
committed
fix: full depth tracking across Codable container hierarchy
This change includes 2 main changes. Previously, the `maximumDepth` option was defined but not actually used in `CodableCBORDecoder`, creating a DoS vulnerability where deeply nested structures could cause stack overflow. **Problem:** 1. `CodableCBORDecoder` had a `maximumDepth` property but it wasn't included in the options computed property, so it defaulted to `.max` 2. `SingleValueDecodingContainer` called `CBOR.decode()` without passing options 3. `KeyedDecodingContainer` and `UnkeyedDecodingContainer` created `CBORDecoder` instances without passing options 4. This meant depth checking was never enforced for Codable decoding **Fix:** - Add `maximumDepth` property to `CodableCBORDecoder` (public API) - Include `maximumDepth` in options computed property - Update `setOptions()` to set `maximumDepth` - Pass options to all `CBOR.decode()` calls in `SingleValueDecodingContainer` - Pass options to `CBORDecoder()` calls in `KeyedDecodingContainer` - Pass options to `CBORDecoder()` calls in `UnkeyedDecodingContainer` Changes: - Added `currentDepth` field to `_CBORDecoder` and all container classes (`KeyedContainer`, `UnkeyedContainer`, `SingleValueContainer`) - Thread `currentDepth` through all container creation and nested decoding - Check depth before creating keyed/unkeyed containers in `_CBORDecoder` - Increment depth when creating nested decoders in `decode()` methods - Pass incremented depth through `superDecoder()` methods Previously, each `CBOR.decode()` call would reset `currentDepth` to 0, allowing deeply nested structures to bypass the depth limit. For example, a 10-level nested array with `maximumDepth=5` would incorrectly succeed. Now depth is properly tracked across the entire container hierarchy.
1 parent 6b33977 commit 12757c5

5 files changed

Lines changed: 247 additions & 43 deletions

File tree

Sources/Decoder/CodableCBORDecoder.swift

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Foundation
33
final public class CodableCBORDecoder {
44
public var useStringKeys: Bool = false
55
public var dateStrategy: DateStrategy = .taggedAsEpochTimestamp
6+
public var maximumDepth: Int = .max
67

78
struct _Options {
89
let useStringKeys: Bool
@@ -29,7 +30,7 @@ final public class CodableCBORDecoder {
2930
}
3031

3132
var options: _Options {
32-
return _Options(useStringKeys: self.useStringKeys, dateStrategy: self.dateStrategy)
33+
return _Options(useStringKeys: self.useStringKeys, dateStrategy: self.dateStrategy, maximumDepth: self.maximumDepth)
3334
}
3435

3536
public init() {}
@@ -66,6 +67,7 @@ final public class CodableCBORDecoder {
6667
func setOptions(_ newOptions: _Options) {
6768
self.useStringKeys = newOptions.useStringKeys
6869
self.dateStrategy = newOptions.dateStrategy
70+
self.maximumDepth = newOptions.maximumDepth
6971
}
7072
}
7173

@@ -78,34 +80,52 @@ final class _CBORDecoder {
7880
fileprivate var data: ArraySlice<UInt8>
7981

8082
let options: CodableCBORDecoder._Options
83+
var currentDepth: Int
8184

82-
init(data: ArraySlice<UInt8>, options: CodableCBORDecoder._Options) {
85+
init(data: ArraySlice<UInt8>, options: CodableCBORDecoder._Options, currentDepth: Int = 0) {
8386
self.data = data
8487
self.options = options
88+
self.currentDepth = currentDepth
8589
}
8690
}
8791

8892
extension _CBORDecoder: Decoder {
8993
func container<Key: CodingKey>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
94+
guard self.currentDepth < self.options.maximumDepth else {
95+
let context = DecodingError.Context(
96+
codingPath: self.codingPath,
97+
debugDescription: "Maximum decoding depth of \(self.options.maximumDepth) exceeded"
98+
)
99+
throw DecodingError.dataCorrupted(context)
100+
}
101+
90102
try ensureMap(self.data.first, keyType: Key.self)
91103

92-
let container = KeyedContainer<Key>(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options)
104+
let container = KeyedContainer<Key>(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth)
93105
self.container = container
94106

95107
return KeyedDecodingContainer(container)
96108
}
97109

98110
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
111+
guard self.currentDepth < self.options.maximumDepth else {
112+
let context = DecodingError.Context(
113+
codingPath: self.codingPath,
114+
debugDescription: "Maximum decoding depth of \(self.options.maximumDepth) exceeded"
115+
)
116+
throw DecodingError.dataCorrupted(context)
117+
}
118+
99119
try ensureArray(self.data.first)
100120

101-
let container = UnkeyedContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options)
121+
let container = UnkeyedContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth)
102122
self.container = container
103123

104124
return container
105125
}
106126

107127
func singleValueContainer() throws -> SingleValueDecodingContainer {
108-
let container = SingleValueContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options)
128+
let container = SingleValueContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth)
109129
self.container = container
110130

111131
return container

Sources/Decoder/KeyedDecodingContainer.swift

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ extension _CBORDecoder {
1414
var codingPath: [CodingKey]
1515
var userInfo: [CodingUserInfoKey: Any]
1616
let options: CodableCBORDecoder._Options
17+
let currentDepth: Int
1718

18-
init(data: ArraySlice<UInt8>, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], options: CodableCBORDecoder._Options) {
19+
init(data: ArraySlice<UInt8>, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], options: CodableCBORDecoder._Options, currentDepth: Int = 0) {
1920
self.codingPath = codingPath
2021
self.userInfo = userInfo
2122
self.data = data
2223
self.index = self.data.startIndex
2324
self.options = options
25+
self.currentDepth = currentDepth
2426
}
2527

2628
func checkCanDecodeValue(forKey key: Key) throws {
@@ -41,7 +43,7 @@ extension _CBORDecoder {
4143

4244
var nestedContainers: [AnyCodingKey: CBORDecodingContainer] = [:]
4345

44-
let unkeyedContainer = UnkeyedContainer(data: self.data.suffix(from: self.index), codingPath: self.codingPath, userInfo: self.userInfo, options: self.options)
46+
let unkeyedContainer = UnkeyedContainer(data: self.data.suffix(from: self.index), codingPath: self.codingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth)
4547
unkeyedContainer.count = count * 2
4648

4749
var iterator = unkeyedContainer.nestedContainers.makeIterator()
@@ -94,7 +96,7 @@ extension _CBORDecoder {
9496
// each key-value pair in the map.
9597
let nextIndex = self.data.startIndex.advanced(by: 1)
9698
let remainingData = self.data.suffix(from: nextIndex)
97-
count = try? CBORDecoder(input: remainingData.map { $0 }).readPairsUntilBreak().keys.count
99+
count = try? CBORDecoder(input: remainingData.map { $0 }, options: self.options.toCBOROptions()).readPairsUntilBreak().keys.count
98100
default:
99101
let context = DecodingError.Context(
100102
codingPath: self.codingPath,
@@ -145,9 +147,10 @@ extension _CBORDecoder.KeyedContainer: KeyedDecodingContainerProtocol {
145147
try checkCanDecodeValue(forKey: key)
146148

147149
let container = try self.nestedContainers()[anyCodingKeyForKey(key)]!
148-
let decoder = CodableCBORDecoder()
149-
decoder.setOptions(self.options)
150-
return try decoder.decode(T.self, from: container.data)
150+
let innerDecoder = _CBORDecoder(data: container.data, options: self.options, currentDepth: self.currentDepth + 1)
151+
innerDecoder.codingPath = self.codingPath + [key]
152+
innerDecoder.userInfo = self.userInfo
153+
return try T(from: innerDecoder)
151154
}
152155

153156
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
@@ -170,17 +173,18 @@ extension _CBORDecoder.KeyedContainer: KeyedDecodingContainerProtocol {
170173
data: anyCodingKeyedContainer.data,
171174
codingPath: anyCodingKeyedContainer.codingPath,
172175
userInfo: anyCodingKeyedContainer.userInfo,
173-
options: anyCodingKeyedContainer.options
176+
options: anyCodingKeyedContainer.options,
177+
currentDepth: anyCodingKeyedContainer.currentDepth
174178
)
175179
return KeyedDecodingContainer(container)
176180
}
177181

178182
func superDecoder() throws -> Decoder {
179-
return _CBORDecoder(data: self.data, options: self.options)
183+
return _CBORDecoder(data: self.data, options: self.options, currentDepth: self.currentDepth + 1)
180184
}
181185

182186
func superDecoder(forKey key: Key) throws -> Decoder {
183-
let decoder = _CBORDecoder(data: self.data, options: self.options)
187+
let decoder = _CBORDecoder(data: self.data, options: self.options, currentDepth: self.currentDepth + 1)
184188
decoder.codingPath = [key]
185189

186190
return decoder

Sources/Decoder/SingleValueDecodingContainer.swift

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ extension _CBORDecoder {
77
var data: ArraySlice<UInt8>
88
var index: Data.Index
99
let options: CodableCBORDecoder._Options
10+
let currentDepth: Int
1011

11-
init(data: ArraySlice<UInt8>, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], options: CodableCBORDecoder._Options) {
12+
init(data: ArraySlice<UInt8>, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], options: CodableCBORDecoder._Options, currentDepth: Int = 0) {
1213
self.codingPath = codingPath
1314
self.userInfo = userInfo
1415
self.data = data
1516
self.index = self.data.startIndex
1617
self.options = options
18+
self.currentDepth = currentDepth
1719
}
1820

1921
func checkCanDecode<T>(_ type: T.Type, format: UInt8) throws {
@@ -32,7 +34,7 @@ extension _CBORDecoder {
3234

3335
extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
3436
func decodeNil() -> Bool {
35-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
37+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
3638
return false
3739
}
3840
switch cbor {
@@ -42,7 +44,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
4244
}
4345

4446
func decode(_ type: Bool.Type) throws -> Bool {
45-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
47+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
4648
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
4749
throw DecodingError.dataCorrupted(context)
4850
}
@@ -55,7 +57,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
5557
}
5658

5759
func decode(_ type: String.Type) throws -> String {
58-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
60+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
5961
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
6062
throw DecodingError.dataCorrupted(context)
6163
}
@@ -68,7 +70,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
6870
}
6971

7072
func decode(_ type: Double.Type) throws -> Double {
71-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
73+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
7274
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
7375
throw DecodingError.dataCorrupted(context)
7476
}
@@ -83,7 +85,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
8385
}
8486

8587
func decode(_ type: Float.Type) throws -> Float {
86-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
88+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
8789
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
8890
throw DecodingError.dataCorrupted(context)
8991
}
@@ -97,7 +99,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
9799
}
98100

99101
func decode(_ type: Int.Type) throws -> Int {
100-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
102+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
101103
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
102104
throw DecodingError.dataCorrupted(context)
103105
}
@@ -111,7 +113,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
111113
}
112114

113115
func decode(_ type: Int8.Type) throws -> Int8 {
114-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
116+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
115117
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
116118
throw DecodingError.dataCorrupted(context)
117119
}
@@ -125,7 +127,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
125127
}
126128

127129
func decode(_ type: Int16.Type) throws -> Int16 {
128-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
130+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
129131
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
130132
throw DecodingError.dataCorrupted(context)
131133
}
@@ -139,7 +141,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
139141
}
140142

141143
func decode(_ type: Int32.Type) throws -> Int32 {
142-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
144+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
143145
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
144146
throw DecodingError.dataCorrupted(context)
145147
}
@@ -153,7 +155,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
153155
}
154156

155157
func decode(_ type: Int64.Type) throws -> Int64 {
156-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
158+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
157159
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
158160
throw DecodingError.dataCorrupted(context)
159161
}
@@ -167,7 +169,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
167169
}
168170

169171
func decode(_ type: UInt.Type) throws -> UInt {
170-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
172+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
171173
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
172174
throw DecodingError.dataCorrupted(context)
173175
}
@@ -180,7 +182,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
180182
}
181183

182184
func decode(_ type: UInt8.Type) throws -> UInt8 {
183-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
185+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
184186
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
185187
throw DecodingError.dataCorrupted(context)
186188
}
@@ -193,7 +195,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
193195
}
194196

195197
func decode(_ type: UInt16.Type) throws -> UInt16 {
196-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
198+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
197199
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
198200
throw DecodingError.dataCorrupted(context)
199201
}
@@ -206,7 +208,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
206208
}
207209

208210
func decode(_ type: UInt32.Type) throws -> UInt32 {
209-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
211+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
210212
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
211213
throw DecodingError.dataCorrupted(context)
212214
}
@@ -219,7 +221,7 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
219221
}
220222

221223
func decode(_ type: UInt64.Type) throws -> UInt64 {
222-
guard let cbor = try? CBOR.decode(self.data.map { $0 }) else {
224+
guard let cbor = try? CBOR.decode(self.data.map { $0 }, options: self.options.toCBOROptions()) else {
223225
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid format: \(self.data)")
224226
throw DecodingError.dataCorrupted(context)
225227
}
@@ -232,7 +234,9 @@ extension _CBORDecoder.SingleValueContainer: SingleValueDecodingContainer {
232234
}
233235

234236
func decode<T: Decodable>(_ type: T.Type) throws -> T {
235-
let decoder = _CBORDecoder(data: self.data, options: self.options)
237+
let decoder = _CBORDecoder(data: self.data, options: self.options, currentDepth: self.currentDepth + 1)
238+
decoder.codingPath = self.codingPath
239+
decoder.userInfo = self.userInfo
236240
let value = try T(from: decoder)
237241
if let nextIndex = decoder.container?.index {
238242
self.index = nextIndex

0 commit comments

Comments
 (0)