diff --git a/Sources/_RegexParser/Regex/Parse/Diagnostics.swift b/Sources/_RegexParser/Regex/Parse/Diagnostics.swift index 53b2290e9..dd1a87c9b 100644 --- a/Sources/_RegexParser/Regex/Parse/Diagnostics.swift +++ b/Sources/_RegexParser/Regex/Parse/Diagnostics.swift @@ -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 @@ -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" } } } diff --git a/Sources/_RegexParser/Regex/Parse/Parse.swift b/Sources/_RegexParser/Regex/Parse/Parse.swift index 1fdadd8de..e11109dff 100644 --- a/Sources/_RegexParser/Regex/Parse/Parse.swift +++ b/Sources/_RegexParser/Regex/Parse/Parse.swift @@ -91,7 +91,7 @@ struct ParsingContext { } } - fileprivate var maxParseDepth: Int { 64 } + internal static var maxParseDepth: Int { 64 } init(syntax: SyntaxOptions) { self.syntax = syntax @@ -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. /// @@ -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))) } @@ -523,6 +529,15 @@ extension Parser { mutating func parseCustomCharacterClass( _ start: Source.Located ) -> 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)) + } + let alreadyInCCC = context.isInCustomCharacterClass context.isInCustomCharacterClass = true defer { context.isInCustomCharacterClass = alreadyInCCC } diff --git a/Tests/RegexTests/ParseTests.swift b/Tests/RegexTests/ParseTests.swift index cbb9e6b32..d0a176148 100644 --- a/Tests/RegexTests/ParseTests.swift +++ b/Tests/RegexTests/ParseTests.swift @@ -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() {