package support; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; /** * MOAD-0011: CWE-1333 — A Catastrophic Inheritance. * * Defect: a PCRE-compatible regex engine inherits Perl's backtracking NFA * semantics. Applied to user-controlled input with nested quantifiers or * ambiguous alternation, the engine explores O(2^N) parse trees before * concluding no match. The defect propagates through intellectual lineage: * Perl designed the backtracking NFA in 1987, PCRE codified it, and every * language that adopted Perl's regex syntax inherited the worst-case complexity. * * Confirmed instance: bleach 3.x sanitize_css gauntlet pattern — 12-second hang * on 71-char input. Java's java.util.regex uses backtracking NFA and exhibits * the same catastrophic behavior on the same class of patterns. * * Fix: restructure the pattern to eliminate nested quantifiers and ambiguous * alternation, or switch to a linear NFA engine (RE2, Rust regex) that * guarantees O(N) matching with no catastrophic backtracking. * * This specimen proves the defect structurally (nested quantifier detection) * and behaviorally at safe scale (step counting on a small custom NFA). * * Scanner detects: Pattern.compile / re.compile / RegExp constructor called with * patterns containing nested quantifiers: (X+)+, (X*)+, (X+)*, (X|Y+)+, etc. */ public class Moad0011Algorithm { // ── Structural detection ────────────────────────────────────────────────── /** * Returns true if the regex pattern string contains nested quantifiers — * the structural signature of catastrophic backtracking risk. * * Detects patterns of the form (group-with-quantifier) followed by +, *, ?, {n,}. * Examples that match: (a+)+ (a*)+ (a+b*)+ (a+)+? (a+){2,} * Examples that do not: a+ (a+) (a|b)+ [a-z]+ */ public static boolean hasCatastrophicStructure(String pattern) { // Regex to detect nested quantifiers: // A group whose contents contain a quantifier, followed by an outer quantifier. // Inner: \([^)]*[+*?][^)]*\) — a paren group containing +, *, or ? // Outer: [+*?] or {\d+,?\d*} following the closing paren return pattern.matches(".*\\([^)]*[+*?][^)]*\\)[+*?{].*"); } /** * Returns true if the pattern is safe: no nested quantifiers detected. */ public static boolean isSafePattern(String pattern) { return !hasCatastrophicStructure(pattern); } // ── Known defective and fixed pattern pairs ─────────────────────────────── /** * Defective: (a+)+b — nested quantifier. * The outer + allows grouping any prefix of a's; the inner + requires at * least one. On input "aaa...c" (no 'b'), the engine explores all ways to * partition the a-run into groups — O(2^N) attempts. */ public static final String DEFECTIVE_PATTERN_BLEACH = "(a+)+b"; /** * Fixed: a+b — equivalent semantics, no nesting. * Matches one or more a's followed by b. On "aaa...c" it fails in O(N). */ public static final String FIXED_PATTERN_BLEACH = "a+b"; /** * Defective: CSS-style gauntlet (simplified from bleach 3.x). * Ambiguous alternation with nested quantifiers across CSS property branches. */ public static final String DEFECTIVE_PATTERN_CSS = "([a-z0-9]+[\\s]*:[\\s]*(([a-z0-9]+[\\s]*)+(,|;|$))+)"; /** * Fixed: anchored, non-ambiguous CSS value pattern. */ public static final String FIXED_PATTERN_CSS = "^[a-z0-9][a-z0-9\\s]*:[a-z0-9,;\\s]+$"; // ── Behavioral proof: step-counting NFA for (a+)+b ─────────────────────── /** * Result of a regex match attempt: whether it matched and how many NFA * steps were taken. Step count proves exponential vs linear growth. */ public static final class Result { public final boolean matched; public final long steps; public Result(boolean matched, long steps) { this.matched = matched; this.steps = steps; } } /** * Defective NFA: matches (a+)+b with explicit backtracking step counting. * * Implements the nested quantifier pattern by trying all possible * partitions of a leading run of 'a' characters. On non-matching input * (no trailing 'b'), explores O(2^N) states. * * @param input the string to match * @return Result with matched=true iff input matches, steps=NFA steps taken */ public static Result matchDefective(String input) { long[] stepCounter = {0L}; boolean matched = defectiveMatch(input, 0, stepCounter); return new Result(matched, stepCounter[0]); } /** * Recursive backtracking NFA for (a+)+b. * * State: consumed 'pos' characters of input. Outer quantifier tries at * least one inner (a+) group. Each inner group consumes 1..k a's. The * outer loop repeats until no more a's can be consumed, then checks for 'b'. */ private static boolean defectiveMatch(String input, int pos, long[] steps) { steps[0]++; return outerPlus(input, pos, steps); } /** Outer (...)+ — must match at least one inner group. */ private static boolean outerPlus(String input, int pos, long[] steps) { // Try consuming one inner (a+) group starting at 'pos', then recurse outer. return innerPlus(input, pos, steps, true); } /** * Inner a+ — consume 1..k a's, then try continuing the outer loop or matching 'b'. * * @param first true if this is the first attempt at the outer loop (must match at least once) */ private static boolean innerPlus(String input, int pos, long[] steps, boolean first) { steps[0]++; if (pos >= input.length() || input.charAt(pos) != 'a') { // Cannot consume an 'a' — try ending outer loop and matching 'b' if (!first) { return matchB(input, pos, steps); } return false; // outer + needs at least one inner group } // Greedy: try consuming as many a's as possible, then backtrack int start = pos; // Count maximum a's from here int maxPos = pos; while (maxPos < input.length() && input.charAt(maxPos) == 'a') maxPos++; // Try splitting: consume (maxPos - start) down to 1 a's in inner group, // then continue outer loop or end for (int endInner = maxPos; endInner > start; endInner--) { steps[0]++; // After consuming [start..endInner) a's, try continuing outer loop if (innerPlus(input, endInner, steps, false)) return true; } return false; } /** Match literal 'b' at position pos. */ private static boolean matchB(String input, int pos, long[] steps) { steps[0]++; return pos < input.length() && input.charAt(pos) == 'b' && pos + 1 == input.length(); } /** * Fixed NFA: matches a+b in O(N) steps — no backtracking, no nested quantifiers. * * @param input the string to match * @return Result with matched=true iff input matches a+b exactly, steps=O(N) */ public static Result matchFixed(String input) { long steps = 0; int pos = 0; // Must have at least one 'a' if (pos >= input.length() || input.charAt(pos) != 'a') { return new Result(false, ++steps); } // Consume all leading a's — O(N) scan, single pass, no backtracking while (pos < input.length() && input.charAt(pos) == 'a') { pos++; steps++; } // Must end with exactly one 'b' and nothing after steps++; boolean matched = (pos < input.length() && input.charAt(pos) == 'b' && pos + 1 == input.length()); return new Result(matched, steps); } /** * Build an adversarial non-matching input: N a's followed by 'c'. * Defective engine explores O(2^N) states; fixed engine scans O(N) and stops. */ public static String adversarialInput(int aCount) { return "a".repeat(aCount) + "c"; } /** * Build a matching input: N a's followed by 'b'. * Both engines return matched=true; step counts differ in constant factor. */ public static String matchingInput(int aCount) { return "a".repeat(aCount) + "b"; } }