Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Sources/_RegexParser/Regex/Parse/Diagnostics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ enum ParseError: Error, Hashable {

case expectedCalloutArgument

// Excessively nested groups (i.e. recursion)
// Excessively nested groups or character classes (i.e. recursion)
case nestingTooDeep

// MARK: Semantic Errors
Expand Down Expand Up @@ -246,7 +246,7 @@ extension ParseError: CustomStringConvertible {
return "expression is not quantifiable"

case .nestingTooDeep:
return "group is too deeply nested"
return "group or character class is too deeply nested"
}
}
}
Expand Down
31 changes: 23 additions & 8 deletions Sources/_RegexParser/Regex/Parse/Parse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ struct ParsingContext {
}
}

fileprivate var maxParseDepth: Int { 64 }
internal static var maxParseDepth: Int { 64 }

init(syntax: SyntaxOptions) {
self.syntax = syntax
Expand Down Expand Up @@ -186,6 +186,17 @@ extension Parser {
return .init(ast, globalOptions: opts, diags: diags)
}

/// Diagnose excessive nesting and stop parsing.
///
/// Excessive nesting isn't recoverable and can lead to excessive work
/// downstream, so we stop parsing immediately on this type of error
/// by just consuming the remainder of the input.
mutating func endParsingExcessiveNesting() {
self.errorAtCurrentPosition(.nestingTooDeep)
diags.suppressFurtherDiagnostics = true
src.bounds = src.bounds.upperBound ..< src.bounds.upperBound
}

/// Parse a regular expression node. This should be used instead of `parse()`
/// for recursive calls.
///
Expand All @@ -197,13 +208,8 @@ extension Parser {
// our recursion.
context.parseDepth += 1
defer { context.parseDepth -= 1 }
guard context.parseDepth < context.maxParseDepth else {
self.errorAtCurrentPosition(.nestingTooDeep)

// This is not generally recoverable and further errors will be
// incorrect
diags.suppressFurtherDiagnostics = true

guard context.parseDepth <= ParsingContext.maxParseDepth else {
defer { endParsingExcessiveNesting() }
return .empty(.init(loc(src.currentPosition)))
Comment thread
natecook1000 marked this conversation as resolved.
Resolved
}

Expand Down Expand Up @@ -523,6 +529,15 @@ extension Parser {
mutating func parseCustomCharacterClass(
_ start: Source.Located<CustomCC.Start>
) -> CustomCC {
// Excessively nested character classes are subject to the same recursion
// limit as groups.
context.parseDepth += 1
defer { context.parseDepth -= 1 }
guard context.parseDepth <= ParsingContext.maxParseDepth else {
endParsingExcessiveNesting()
return CustomCC(start, [], loc(start.location.start))
Comment thread
natecook1000 marked this conversation as resolved.
Resolved
}

let alreadyInCCC = context.isInCustomCharacterClass
context.isInCustomCharacterClass = true
defer { context.isInCustomCharacterClass = alreadyInCCC }
Expand Down
34 changes: 24 additions & 10 deletions Tests/RegexTests/ParseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3323,18 +3323,32 @@ extension RegexTests {
}

func testMaliciousNesting() {
// Excessively nested subpatterns is a common DOS attack
diagnosticTest(
String(repeating: "(", count: 500)
func pattern(prefix: String, suffix: String, depth: Int) -> String {
String(repeating: prefix, count: depth)
+ "a"
+ String(repeating: ")*", count: 500),
.nestingTooDeep)
+ String(repeating: suffix, count: depth)
}

// Excessively nested subpatterns is a common DOS attack
for depth in [ParsingContext.maxParseDepth, 100, 1_000, 5_000] {
diagnosticTest(
pattern(prefix: "[", suffix: "]", depth: depth),
.nestingTooDeep)
diagnosticTest(
pattern(prefix: "(", suffix: ")*", depth: depth),
.nestingTooDeep)
diagnosticTest(
pattern(prefix: "(?:", suffix: ")*", depth: depth),
.nestingTooDeep)
diagnosticTest(
pattern(prefix: "[a-z&&", suffix: "]", depth: depth),
.nestingTooDeep)
}

diagnosticTest(
String(repeating: "(?:", count: 500)
+ "a"
+ String(repeating: ")*", count: 500),
.nestingTooDeep)
// Reasonable nesting is still fine.
for depth in [1, 10, 30, ParsingContext.maxParseDepth - 1] {
diagnosticTest(pattern(prefix: "[", suffix: "]", depth: depth))
}
}

func testDelimiterLexingErrors() {
Expand Down
Loading