Each MOAD now has a synthetic defective specimen and fixed specimen proven from first principles across three test tiers: Unit (tests/unit/Moad000X*.java): - Correctness: defective and fixed produce identical functional output - Defect behavior: defective specimen exhibits the defect (measurable) - Fix behavior: fixed specimen eliminates the defect Integration (tests/integration/AllMoadsIntegrationTest.java): - All 9 MOADs proven at medium scale (N=500-2000) - MOAD-0001: O(N^2) vs O(N) list scan at N=1000 - MOAD-0002: 500 sessions trample each other (defective) vs coexist (fixed) - MOAD-0003: 250 anonymous requests leak auth identity (defective) vs zero (fixed) - MOAD-0004: 3000 credential exposures across 1000 requests (defective) vs zero (fixed) - MOAD-0005: 500 computes for 500 concurrent misses vs exactly 1 - MOAD-0006: all 500 passwords extractable from DB (defective) vs unextractable (fixed) - MOAD-0007: N=2000 spatial objects, defective visits all 2000 vs O(log N + k) - MOAD-0009: 990 wasted firings for 1000 ticks / 10 events vs zero waste - MOAD-0011: 10240 NFA steps vs 13 steps on N=12 adversarial input (788x) Functional (tests/functional/AllMoadsFunctionalTest.java): - MOAD-0005: real-thread contention proves herd (defective >1 compute, fixed exactly 1) - MOAD-0007: N=50000 spatial objects, 50M defective probes vs 516K fixed (97x speedup) - MOAD-0009: 10000 ticks / 10 events, 9990 wasted firings vs zero (1000x ratio) - MOAD-0011: N=16 adversarial, 163840 defective steps vs 17 fixed (9638x ratio) Support algorithms (tests/support/Moad000X*.java): - Moad0002Algorithm: shared mutable global state (DefectiveAudioSystem / FixedAudioSystem + Context) - Moad0003Algorithm: ThreadLocal not cleared (handleDefective / handleFixed with finally) - Moad0004Algorithm: HTTP headers logged verbatim (logDefective / logFixed with CREDENTIAL_HEADERS denylist) - Moad0005Algorithm: get+null+compute+put (DefectiveCache HashMap / FixedCache ConcurrentHashMap.computeIfAbsent) - Moad0006Algorithm: Base64 password storage (DefectiveCredentialStore / FixedCredentialStore SHA-256+salt) - Moad0007Algorithm: linear spatial scan (queryDefective list / queryFixed sorted array + binary search) - Moad0009Algorithm: timer-driven polling (runDefectiveScheduler / runFixedEventDriven) - Moad0011Algorithm: PCRE nested quantifiers (matchDefective backtracking NFA / matchFixed linear NFA) Makefile: added unit-moad-0002 through unit-moad-0011 targets, integration-all-moads, functional-all-moads. integration and functional targets now depend on all-MOADs variants.
216 lines
8.5 KiB
Java
216 lines
8.5 KiB
Java
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";
|
|
}
|
|
}
|