Skip to content

Commit c6ec600

Browse files
authored
Limit recursion in regex parser (#754)
1 parent 1031c9d commit c6ec600

File tree

3 files changed

+54
-0
lines changed

3 files changed

+54
-0
lines changed

Sources/_RegexParser/Regex/Parse/Diagnostics.swift

+20
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ enum ParseError: Error, Hashable {
8787

8888
case expectedCalloutArgument
8989

90+
// Excessively nested groups (i.e. recursion)
91+
case nestingTooDeep
92+
9093
// MARK: Semantic Errors
9194

9295
case unsupported(String)
@@ -241,6 +244,9 @@ extension ParseError: CustomStringConvertible {
241244
return "character '\(lhs)' must compare less than or equal to '\(rhs)'"
242245
case .notQuantifiable:
243246
return "expression is not quantifiable"
247+
248+
case .nestingTooDeep:
249+
return "group is too deeply nested"
244250
}
245251
}
246252
}
@@ -302,25 +308,39 @@ extension Diagnostic {
302308
public struct Diagnostics: Hashable {
303309
public private(set) var diags = [Diagnostic]()
304310

311+
// In the event of an unrecoverable parse error, set this
312+
// to avoid emitting spurious diagnostics.
313+
internal var suppressFurtherDiagnostics = false
314+
305315
public init() {}
306316
public init(_ diags: [Diagnostic]) {
307317
self.diags = diags
308318
}
309319

310320
/// Add a new diagnostic to emit.
311321
public mutating func append(_ diag: Diagnostic) {
322+
guard !suppressFurtherDiagnostics else {
323+
return
324+
}
312325
diags.append(diag)
313326
}
314327

315328
/// Add all the diagnostics of another diagnostic collection.
316329
public mutating func append(contentsOf other: Diagnostics) {
330+
guard !suppressFurtherDiagnostics else {
331+
return
332+
}
317333
diags.append(contentsOf: other.diags)
318334
}
319335

320336
/// Add all the new fatal error diagnostics of another diagnostic collection.
321337
/// This assumes that `other` was the same as `self`, but may have additional
322338
/// diagnostics added to it.
323339
public mutating func appendNewFatalErrors(from other: Diagnostics) {
340+
guard !suppressFurtherDiagnostics else {
341+
return
342+
}
343+
324344
let newDiags = other.diags.dropFirst(diags.count)
325345
for diag in newDiags where diag.behavior == .fatalError {
326346
append(diag)

Sources/_RegexParser/Regex/Parse/Parse.swift

+19
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ struct ParsingContext {
7373
/// A set of used group names.
7474
private var usedGroupNames = Set<String>()
7575

76+
/// The depth of calls to parseNode (recursion depth plus 1)
77+
fileprivate var parseDepth = 0
78+
7679
/// The syntax options currently set.
7780
fileprivate(set) var syntax: SyntaxOptions
7881

@@ -88,6 +91,8 @@ struct ParsingContext {
8891
}
8992
}
9093

94+
fileprivate var maxParseDepth: Int { 64 }
95+
9196
init(syntax: SyntaxOptions) {
9297
self.syntax = syntax
9398
}
@@ -188,6 +193,20 @@ extension Parser {
188193
/// Alternation -> Concatenation ('|' Concatenation)*
189194
///
190195
mutating func parseNode() -> AST.Node {
196+
// Excessively nested groups is a common DOS attack, so limit
197+
// our recursion.
198+
context.parseDepth += 1
199+
defer { context.parseDepth -= 1 }
200+
guard context.parseDepth < context.maxParseDepth else {
201+
self.errorAtCurrentPosition(.nestingTooDeep)
202+
203+
// This is not generally recoverable and further errors will be
204+
// incorrect
205+
diags.suppressFurtherDiagnostics = true
206+
207+
return .empty(.init(loc(src.currentPosition)))
208+
}
209+
191210
let _start = src.currentPosition
192211

193212
if src.isEmpty { return .empty(.init(loc(_start))) }

Tests/RegexTests/ParseTests.swift

+15
Original file line numberDiff line numberDiff line change
@@ -3322,6 +3322,21 @@ extension RegexTests {
33223322
diagnosticTest("(*LIMIT_DEPTH=-1", .expectedNumber("", kind: .decimal), .expected(")"), unsupported: true)
33233323
}
33243324

3325+
func testMaliciousNesting() {
3326+
// Excessively nested subpatterns is a common DOS attack
3327+
diagnosticTest(
3328+
String(repeating: "(", count: 500)
3329+
+ "a"
3330+
+ String(repeating: ")*", count: 500),
3331+
.nestingTooDeep)
3332+
3333+
diagnosticTest(
3334+
String(repeating: "(?:", count: 500)
3335+
+ "a"
3336+
+ String(repeating: ")*", count: 500),
3337+
.nestingTooDeep)
3338+
}
3339+
33253340
func testDelimiterLexingErrors() {
33263341

33273342
// MARK: Printable ASCII

0 commit comments

Comments
 (0)