Package groovy.util.regex
Class RegexGuard
java.lang.Object
groovy.util.regex.RegexGuard
Deadline-guarded regular expression evaluation, protecting against
Regular Expression Denial of Service (ReDoS) when patterns or inputs
come from untrusted sources. Java's regex engine has no native timeout,
so catastrophic backtracking can hang a thread indefinitely.
The guard works by wrapping the input CharSequence so that
charAt periodically checks a deadline and throws
RegexTimeoutException once it has passed. Backtracking repeatedly
re-reads input characters, so a runaway match is cut off shortly after the
deadline without needing watchdog threads or thread interruption.
import groovy.util.regex.RegexGuard
import groovy.util.regex.RegexTimeoutException
import java.time.Duration
assert RegexGuard.matches(/gro+vy/, 'groovy', 200)
def m = RegexGuard.matcher(/(\d+)/, 'abc 123', Duration.ofMillis(200))
assert m.find() && m.group(1) == '123'
try {
RegexGuard.matches(/(.*,){16}X/, '1,' * 40, 200) // catastrophic backtracking
assert false, 'should have timed out'
} catch (RegexTimeoutException expected) {
}
Notes and limitations:
- The deadline starts when the guarded matcher is created and covers all
subsequent use of it, e.g. repeated
find()calls. - The clock is only consulted while the engine reads input characters, every 512 reads; evaluations finishing in fewer reads never pay for a clock call. Pathological patterns perform millions of reads per second, so overshoot past the deadline is negligible in practice.
Pattern.compileitself is not guarded; pattern compilation is not subject to backtracking.
- Since:
- 6.0.0
- See Also:
-
Method Summary
Modifier and TypeMethodDescriptionstatic CharSequenceambientGuard(CharSequence input) Runtime hook used by Groovy's regex operators and extension methods: wraps the input so that reads are guarded when an ambient guard (seeguard(long, Closure)) is active on the current thread, and returns the input unchanged otherwise.static MatcherRuntime hook used by theSafeRegextransform: equivalent tomatcher(Object, Object, long)but with the operands in the=~operator's order, so that a rewritten expression still evaluates its operands left to right, exactly as the original did.static <T> TRuns the closure with an ambient regex timeout active on the current thread and returns the closure's result.static CharSequenceguard(CharSequence input, long millis) Wraps a character sequence so that reads beyond the deadline throwRegexTimeoutException.static CharSequenceguard(CharSequence input, Duration timeout) Variant ofguard(CharSequence, long)taking the timeout as aDuration.static <T> TVariant ofguard(long, Closure)taking the timeout as aDuration.static MatcherCreates a matcher of the pattern over deadline-guarded input, like the=~operator.static MatcherVariant ofmatcher(Object, Object, long)taking the timeout as aDuration.static booleanMatches the whole input against the pattern like the==~operator, giving up once the timeout has elapsed.static booleanVariant ofmatches(Object, Object, long)taking the timeout as aDuration.static booleanmatchRegex(Object input, Object pattern, long millis) Runtime hook used by theSafeRegextransform: equivalent tomatches(Object, Object, long)but with the operands in the==~operator's order, so that a rewritten expression still evaluates its operands left to right, exactly as the original did.static CharSequenceprepare(CharSequence input) Runtime hook used by Groovy's regex extension methods: normalizes an input for regex evaluation.
-
Method Details
-
matches
Matches the whole input against the pattern like the==~operator, giving up once the timeout has elapsed. Follows the operator's conventions: anullpattern or input yieldsfalse, non-Patternpatterns and non-Stringinputs are converted via their default string representation, and the matcher is stored forMatcher.lastMatcher.- Parameters:
pattern- the pattern, aPatternor an object whose string representation is the regexinput- the text to matchmillis- the timeout in milliseconds (must be positive)- Returns:
trueif the whole input matches the pattern- Throws:
RegexTimeoutException- if evaluation exceeds the timeout
-
matches
Variant ofmatches(Object, Object, long)taking the timeout as aDuration. -
matcher
Creates a matcher of the pattern over deadline-guarded input, like the=~operator. Matcher operations that read the input, e.g.find()ormatches(), throwRegexTimeoutExceptiononce the deadline, measured from this call, has passed.- Parameters:
pattern- the pattern, aPatternor an object whose string representation is the regexinput- the text to matchmillis- the timeout in milliseconds (must be positive)- Returns:
- a matcher over the guarded input
-
matcher
Variant ofmatcher(Object, Object, long)taking the timeout as aDuration. -
matchRegex
Runtime hook used by theSafeRegextransform: equivalent tomatches(Object, Object, long)but with the operands in the==~operator's order, so that a rewritten expression still evaluates its operands left to right, exactly as the original did.- Parameters:
input- the text to match (the operator's left operand)pattern- the pattern (the operator's right operand)millis- the timeout in milliseconds (must be positive)- Returns:
trueif the whole input matches the pattern- Throws:
RegexTimeoutException- if evaluation exceeds the timeout
-
findRegex
Runtime hook used by theSafeRegextransform: equivalent tomatcher(Object, Object, long)but with the operands in the=~operator's order, so that a rewritten expression still evaluates its operands left to right, exactly as the original did.- Parameters:
input- the text to match (the operator's left operand)pattern- the pattern (the operator's right operand)millis- the timeout in milliseconds (must be positive)- Returns:
- a matcher over the guarded input
-
guard
Wraps a character sequence so that reads beyond the deadline throwRegexTimeoutException. The deadline is attached to the data: it is a single total budget covering every subsequent regex evaluation against the guarded sequence, wherever and on whatever thread it happens. Groovy's regex-evaluatingCharSequenceextension methods (find,findAll,findGroups,eachMatch,replaceAll,switch/casematching, ...) honour the guard, as does anything that passes the sequence through tojava.util.regexuntouched, e.g.Pattern.splitor manual matcher creation. This complementsguard(long, Closure), which instead guards a region of code with a per-evaluation timeout.Note the deadline is only consulted while the regex engine reads input characters, so a well-behaved evaluation over a short input may still succeed after the deadline; runaway evaluations are always cut off. A sequence other than a
String(e.g. aGStringorStringBuilder) is snapshotted viatoString()first, so later mutations of the original are not seen by the guarded view.- Parameters:
input- the sequence to guard (must not benull)millis- the timeout in milliseconds (must be positive)- Returns:
- a guarded view of the input
-
guard
Variant ofguard(CharSequence, long)taking the timeout as aDuration. -
guard
Runs the closure with an ambient regex timeout active on the current thread and returns the closure's result. While active, every regex evaluation reached through Groovy's regex operators (==~,=~, andswitch/case,grepandinagainst aPattern) or theCharSequenceextension methods (find,findAll,findGroups,findAllGroups,eachMatch,matches,replaceAll,replaceFirst,splitEachLine, ...) is guarded: each individual evaluation that exceeds the timeout throwsRegexTimeoutException.import groovy.util.regex.RegexGuard import groovy.util.regex.RegexTimeoutException def groups = RegexGuard.guard(200) { '6.0.0-beta-2'.findGroups(/(\d+)\.(\d+)\.(\d+)(?:-(.+))?/) } assert groups == ['6.0.0-beta-2', '6', '0', '0', 'beta-2'] try { RegexGuard.guard(200) { ('1,' * 40).findGroups(/(.*,){16}X/) // catastrophic backtracking } assert false, 'should have timed out' } catch (RegexTimeoutException expected) { }Semantics:- The timeout applies per evaluation, not to the block as a whole; any number of well-behaved evaluations may run within the scope.
- The scope is dynamic: regex operations in methods called from the closure
are guarded too, as long as they run on the same thread. Threads spawned
within the closure are not covered; on JDK 25+, however, the guard does
propagate into
StructuredTaskScopeforks, which by construction run within the enclosing scope. - Nested guards only tighten: the smaller per-evaluation timeout wins.
- An explicit timeout, e.g. from
matches(Object, Object, long)or aSafeRegexscope, is likewise capped by the ambient timeout. - A matcher created within the scope (e.g. via
=~) keeps its deadline, measured from creation, when used after the block exits. - Direct
java.util.regexcalls, e.g. a hand-writtenpattern.matcher(input), bypass the ambient guard; combine them withguard(CharSequence, long)instead. The same applies to the JDK's ownStringmethods: on aStringreceiver,matches(String),replaceAll(String, String),replaceFirst(String, String)andsplit(String)dispatch tojava.lang.Stringand never enter Groovy's runtime. TheirPattern-argument variants, e.g.str.replaceAll(~/expensive/, 'x'), are extension methods and are covered.
- Parameters:
millis- the per-evaluation timeout in milliseconds (must be positive)closure- the code to run under the ambient guard- Returns:
- the closure's result
-
guard
Variant ofguard(long, Closure)taking the timeout as aDuration. -
ambientGuard
Runtime hook used by Groovy's regex operators and extension methods: wraps the input so that reads are guarded when an ambient guard (seeguard(long, Closure)) is active on the current thread, and returns the input unchanged otherwise.- Parameters:
input- the text a regex is about to be evaluated against- Returns:
- the input, guarded if an ambient guard is active
-
prepare
Runtime hook used by Groovy's regex extension methods: normalizes an input for regex evaluation. AString, or a sequence guarded viaguard(CharSequence, long), is passed through, so an attached deadline is honoured by the evaluation; any other sequence (e.g. aGStringorStringBuilder) is snapshotted viatoString(), as the extension methods have always done. An ambient guard (seeguard(long, Closure)), if active, is then applied on top.- Parameters:
input- the text a regex is about to be evaluated against- Returns:
- the normalized, possibly guarded, input
-