RegexSafetyManager

org.llm4s.security.RegexSafetyManager
@Stable

Safety wrapper for user-supplied regex compilation and matching.

ReDoS is mitigated by two complementary defences:

  1. A cheap pre-screen (safeCompile) rejects null/empty/oversized patterns and a set of well-known catastrophic-backtracking shapes before they are ever compiled.
  2. A hard execution bound (safeFind/safeMatches) caps the number of character accesses the backtracking engine may perform for a single match. Catastrophic backtracking explores an exponential number of paths, each of which reads characters, so a step ceiling deterministically bounds worst-case runtime - even for dangerous patterns the pre-screen misses - without relying on thread interruption or wall-clock timeouts.

The step bound is the real security boundary: any pattern that reaches the matching helpers is safe regardless of how it was constructed. The shape blocklist is only a fast early-reject and must not be relied on for safety.

NOTE: kept intentionally in sync with org.llm4s.runner.WorkspaceRegexSafetyManager, which duplicates this logic because the workspace runner module cannot depend on core. Changes to the heuristics or bounds here should be mirrored there.

Attributes

Graph
Supertypes
class Object
trait Matchable
class Any
Self type

Members list

Type members

Classlikes

final class RegexComplexityException(message: String) extends RuntimeException

Raised when a match exceeds its step budget (likely catastrophic backtracking).

Raised when a match exceeds its step budget (likely catastrophic backtracking).

Attributes

Supertypes
class RuntimeException
class Exception
class Throwable
trait Serializable
class Object
trait Matchable
class Any
Show all

Value members

Concrete methods

def compileLiteral(pattern: String, flags: Int): Pattern

Compiles text as a literal pattern: it is quoted with Pattern.quote, so regex metacharacters in it match themselves.

Compiles text as a literal pattern: it is quoted with Pattern.quote, so regex metacharacters in it match themselves.

It returns the Pattern directly and does not apply the checks of safeCompile. A quoted pattern has no quantifiers or alternation, so the shapes those checks look for cannot occur in it.

Value parameters

flags

java.util.regex.Pattern flags; 0 by default

pattern

the text to match literally

Attributes

Returns

a Pattern that matches pattern as plain text

def compileLiteralCaseInsensitive(pattern: String): Pattern

Compiles text as a literal pattern that ignores case (Pattern.CASE_INSENSITIVE).

Compiles text as a literal pattern that ignores case (Pattern.CASE_INSENSITIVE).

Like compileLiteral, it quotes the text and does not apply the checks of safeCompile.

Value parameters

pattern

the text to match literally

Attributes

Returns

a case-insensitive Pattern that matches pattern as plain text

def replaceAllLiteral(input: String, literalPattern: String, replacement: String, caseInsensitive: Boolean): String

Replaces every occurrence of a literal text in the input.

Replaces every occurrence of a literal text in the input.

Both literalPattern and replacement are taken literally: the pattern is compiled with compileLiteral and the replacement goes through Matcher.quoteReplacement, so $ and \ in it have no special meaning.

Value parameters

caseInsensitive

whether literalPattern matches regardless of case (Pattern.CASE_INSENSITIVE)

input

the text to search

literalPattern

the text to find, matched literally

replacement

the text to put in its place, inserted literally

Attributes

Returns

the input with every occurrence replaced

def replaceFirstLiteral(input: String, literalPattern: String, replacement: String, caseInsensitive: Boolean): String

Replaces the first occurrence of a literal text in the input.

Replaces the first occurrence of a literal text in the input.

It is replaceAllLiteral for the first occurrence only: literalPattern and replacement are both taken literally, the replacement through Matcher.quoteReplacement.

Value parameters

caseInsensitive

whether literalPattern matches regardless of case (Pattern.CASE_INSENSITIVE)

input

the text to search

literalPattern

the text to find, matched literally

replacement

the text to put in its place, inserted literally

Attributes

Returns

the input with its first occurrence replaced, or the input unchanged if there is none

def safeCompile(pattern: String, flags: Int): Either[String, Pattern]

Compiles a user-supplied pattern after the pre-screen, without throwing.

Compiles a user-supplied pattern after the pre-screen, without throwing.

Returns Left with a message when the pattern is null, empty or blank, is longer than 1000 characters, matches one of the known catastrophic-backtracking shapes (nested quantifiers, or a quantified alternation whose alternatives overlap), or is not valid regex syntax. The pre-screen is only an early reject: bound matching with safeFind or safeMatches.

Value parameters

flags

java.util.regex.Pattern flags, such as Pattern.CASE_INSENSITIVE; 0 by default

pattern

the regular expression to compile

Attributes

Returns

the compiled Pattern, or Left with the reason it was rejected

def safeFind(pattern: Pattern, input: String, maxSteps: Long): Either[String, Boolean]

Tells whether the pattern is found anywhere in the input (Matcher.find), within a step budget.

Tells whether the pattern is found anywhere in the input (Matcher.find), within a step budget.

Returns Left with a message when the input is null or longer than 100000 characters, when matching reads more than maxSteps characters (likely catastrophic backtracking), when the pattern recurses too deeply for the input, or when matching fails for another non-fatal reason. See safeMatches for whole-input matching.

Value parameters

input

the text to search

maxSteps

the most character accesses one match may make; 10000000 by default

pattern

the compiled pattern, e.g. from safeCompile

Attributes

Returns

Right(true) if a match is found, Right(false) if not, or Left with the reason matching stopped

def safeMatches(pattern: Pattern, input: String, maxSteps: Long): Either[String, Boolean]

Tells whether the pattern matches the whole input (Matcher.matches), within a step budget.

Tells whether the pattern matches the whole input (Matcher.matches), within a step budget.

It is safeFind for the whole input: find succeeds on a match anywhere in the input, matches only when the pattern matches all of it. The same Left cases apply: a null or over-long input (more than 100000 characters), an exceeded step budget, too deep a recursion, or another non-fatal failure.

Value parameters

input

the text to match

maxSteps

the most character accesses one match may make; 10000000 by default

pattern

the compiled pattern, e.g. from safeCompile

Attributes

Returns

Right(true) if the whole input matches, Right(false) if not, or Left with the reason matching stopped