Skip to content

Commit a714f50

Browse files
authored
Prevent Stack Overflow With Nested Structures (#5)
1 parent 36e1ea2 commit a714f50

File tree

5 files changed

+75
-2
lines changed

5 files changed

+75
-2
lines changed

Sources/PureSwiftJSONParsing/JSONParser.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,21 @@ public struct JSONParser {
1818
@usableFromInline struct JSONParserImpl {
1919

2020
@usableFromInline var reader: DocumentReader
21+
@usableFromInline var depth : Int = 0
2122

2223
@inlinable init<Bytes: Collection>(bytes: Bytes) where Bytes.Element == UInt8 {
2324
self.reader = DocumentReader(bytes: bytes)
2425
}
2526

2627
@usableFromInline mutating func parse() throws -> JSONValue {
2728
let value = try parseValue()
29+
#if DEBUG
30+
defer {
31+
guard self.depth == 0 else {
32+
preconditionFailure()
33+
}
34+
}
35+
#endif
2836

2937
// handle extra character if top level was number
3038
if case .number(_) = value {
@@ -236,13 +244,19 @@ public struct JSONParser {
236244
}
237245

238246
mutating func parseArray() throws -> [JSONValue] {
239-
// parse first value or immidiate end
240-
precondition(reader.value == UInt8(ascii: "["))
247+
assert(reader.value == UInt8(ascii: "["))
248+
guard depth < 512 else {
249+
throw JSONError.tooManyNestedArraysOrDictionaries(characterIndex: reader.index)
250+
}
251+
depth += 1
252+
defer { depth -= 1 }
241253
var state = ArrayState.expectValueOrEnd
242254

243255
var array = [JSONValue]()
244256
array.reserveCapacity(10)
245257

258+
// parse first value or immidiate end
259+
246260
do {
247261
let value = try parseValue()
248262
array.append(value)
@@ -336,6 +350,13 @@ public struct JSONParser {
336350
}
337351

338352
mutating func parseObject() throws -> [String: JSONValue] {
353+
assert(reader.value == UInt8(ascii: "{"))
354+
guard depth < 512 else {
355+
throw JSONError.tooManyNestedArraysOrDictionaries(characterIndex: reader.index)
356+
}
357+
depth += 1
358+
defer { depth -= 1 }
359+
339360
var state = ObjectState.expectKeyOrEnd
340361

341362
// parse first key or end immidiatly

Sources/PureSwiftJSONParsing/JSONValue.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
public enum JSONError: Swift.Error {
33
case unexpectedCharacter(ascii: UInt8)
44
case unexpectedEndOfFile
5+
case tooManyNestedArraysOrDictionaries(characterIndex: Int)
56
}
67

78
public enum JSONValue {

Tests/JSONParsingTests/ArrayParserTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,21 @@ class ArrayParserTests: XCTestCase {
5656
XCTFail("Unexpected error: \(error)")
5757
}
5858
}
59+
60+
func testHighlyNestedArray() throws {
61+
// test 512 should succeed
62+
let passingString = String(repeating: "[", count: 512) + String(repeating: "]", count: 512)
63+
_ = try JSONParser().parse(bytes: [UInt8](passingString.utf8))
64+
65+
let failingString = String(repeating: "[", count: 513)
66+
do {
67+
_ = try JSONParser().parse(bytes: [UInt8](failingString.utf8))
68+
}
69+
catch JSONError.tooManyNestedArraysOrDictionaries(characterIndex: 512) {
70+
//expected case
71+
}
72+
catch {
73+
XCTFail("Unexpected error: \(error)")
74+
}
75+
}
5976
}

Tests/JSONParsingTests/JSONParserTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ class JSONParserTests: XCTestCase {
6565
catch {
6666
XCTFail("Unexpected error: \(error)")
6767
}
68+
}
69+
70+
func testHighlyNestedObjectAndDictionary() throws {
71+
// test 512 should succeed
72+
let passingString = String(repeating: #"{"a":["#, count: 256) + "null" + String(repeating: "]}", count: 256)
73+
_ = try JSONParser().parse(bytes: [UInt8](passingString.utf8))
6874

75+
let failingString = String(repeating: #"{"a":["#, count: 257)
76+
do {
77+
_ = try JSONParser().parse(bytes: [UInt8](failingString.utf8))
78+
}
79+
catch JSONError.tooManyNestedArraysOrDictionaries(characterIndex: 1536) {
80+
//expected case
81+
}
82+
catch {
83+
XCTFail("Unexpected error: \(error)")
84+
}
6985
}
86+
7087
}

Tests/JSONParsingTests/ObjectParserTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,21 @@ class ObjectParserTests: XCTestCase {
2727
let result = try parser.parseObject()
2828
XCTAssertEqual(result, ["hello": .string("world"), "haha": .bool(true)])
2929
}
30+
31+
func testHighlyNestedObject() throws {
32+
// test 512 should succeed
33+
let passingString = String(repeating: #"{"a":"#, count: 512) + "null" + String(repeating: "}", count: 512)
34+
_ = try JSONParser().parse(bytes: [UInt8](passingString.utf8))
35+
36+
let failingString = String(repeating: #"{"a":"#, count: 513)
37+
do {
38+
_ = try JSONParser().parse(bytes: [UInt8](failingString.utf8))
39+
}
40+
catch JSONError.tooManyNestedArraysOrDictionaries(characterIndex: 2560) {
41+
//expected case
42+
}
43+
catch {
44+
XCTFail("Unexpected error: \(error)")
45+
}
46+
}
3047
}

0 commit comments

Comments
 (0)