Guard Regex

Regex-pattern content-safety guard — refuses user-supplied pattern strings that exhibit catastrophic-backtracking (ReDoS) shapes BEFORE the framework compiles them with new RegExp(...). Operator-untrusted patterns flow into search filters, allow-lists, route matchers, and form validators; this primitive screens them so a hostile input can't pin a CPU at 100% inside the regex engine. KIND=identifier; the gate consumes ctx.identifier (or ctx.pattern) and refuses on hostile shapes. Composes with framework parsers (b.safeJson / b.safeBuffer / route helpers) so any operator-fed pattern hits the guard first.

Threat catalog: nested quantifiers ((a+)+, (a*)+, (.+)+ — the canonical ReDoS class, e.g. CVE-2024-21538 cross-spawn and CVE-2022-25929 chartjs-adapter-luxon); alternation-with- quantifier ((a|b)+, (\d|\d{2})*) where alternation overlap amplifies search paths; quantifier-inside-lookaround ((?=.*+), (?!a*)) — catastrophic in some engines; bounded repetition with a large upper bound (gated by maxBoundedRepeat); per-pattern byte cap to defend against parser-stage DoS; BIDI override / zero-width / C0 control / null-byte universal refuse.

Profiles: strict / balanced / permissive. Compliance postures: hipaa / pci-dss / gdpr / soc2. Operators select via { profile: "strict" } or { compliancePosture: "hipaa" }; postures overlay on top of the profile baseline. Nested-quantifier rejection holds at every profile — the catastrophic class is never an operator opt-in.

Pattern strings can't be repaired safely — sanitize either passes through clean input or throws GuardRegexError; the gate returns serve / audit-only / refuse (no sanitize action). Detector regexes themselves are length-bounded by maxPatternBytes so the screener can't be DoS'd by its own inputs.

b.guardRegex.validate(input, opts) #

stable0.7.13hipaapci-dssgdprsoc2
{
  profile:                "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  bidiPolicy:             "reject"|"audit"|"allow",
  controlPolicy:          "reject"|"audit"|"allow",
  nullBytePolicy:         "reject"|"audit"|"allow",
  zeroWidthPolicy:        "reject"|"strip"|"audit"|"allow",
  nestedQuantPolicy:      "reject"|"audit"|"allow",
  alternationQuantPolicy: "reject"|"audit"|"allow",
  boundedRepeatPolicy:    "reject"|"audit"|"allow",
  lookaroundQuantPolicy:  "reject"|"audit"|"allow",
  consecutiveStarPolicy:  "reject"|"audit"|"allow",
  nestedExtglobPolicy:    "reject"|"audit"|"allow",
  inputKind:              "regex"|"glob",
  maxBoundedRepeat:       number,
  maxConsecutiveStars:    number,
  maxPatternBytes:        number,
  maxBytes:               number,
  maxRuntimeMs:           number,
}

Inspect a user-supplied regex pattern string and return an aggregated issue list. Pure inspection — never throws on hostile patterns; caller decides what to do with the issues. The ok flag is true only when zero critical / high issues fire. Throws GuardRegexError("regex.bad-opt") when a numeric opt is non-finite / negative (config-time mistake by the operator).

var clean = b.guardRegex.validate("^[a-z]+$", { profile: "strict" });
clean.ok;                                          // → true

var hostile = b.guardRegex.validate("(a+)+b", { profile: "strict" });
hostile.ok;                                        // → false
hostile.issues.some(function (i) { return i.kind === "nested-quantifier"; });  // → true

b.guardRegex.sanitize(input, opts) #

stable0.7.13hipaapci-dssgdprsoc2
{
  profile:                "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  nestedQuantPolicy:      "reject"|"audit"|"allow",
  alternationQuantPolicy: "reject"|"audit"|"allow",
  boundedRepeatPolicy:    "reject"|"audit"|"allow",
  lookaroundQuantPolicy:  "reject"|"audit"|"allow",
  consecutiveStarPolicy:  "reject"|"audit"|"allow",
  nestedExtglobPolicy:    "reject"|"audit"|"allow",
  inputKind:              "regex"|"glob",
  maxBoundedRepeat:       number,
  maxConsecutiveStars:    number,
  maxPatternBytes:        number,
}

Pass-through-or-throw. Regex patterns cannot be safely repaired (stripping a + from a quantifier silently changes match semantics); this primitive returns the input unchanged when no critical or high issue fires, otherwise throws GuardRegexError with the offending rule id (e.g. regex.nested-quantifier, regex.lookaround-quantifier, regex.bounded-repeat-cap). Operators that need a "best-effort cleanup" semantic should reject the input at the boundary instead.

var safe = b.guardRegex.sanitize("^[a-z]+$", { profile: "strict" });
safe;                                              // → "^[a-z]+$"

try {
  b.guardRegex.sanitize("(a+)+b", { profile: "strict" });
} catch (e) {
  e.code;                                          // → "regex.nested-quantifier"
}

b.guardRegex.gate(opts) #

stable0.7.13hipaapci-dssgdprsoc2
{
  profile:                "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  name:                   string,    // override gate name in audit emissions
  nestedQuantPolicy:      "reject"|"audit"|"allow",
  alternationQuantPolicy: "reject"|"audit"|"allow",
  boundedRepeatPolicy:    "reject"|"audit"|"allow",
  lookaroundQuantPolicy:  "reject"|"audit"|"allow",
  consecutiveStarPolicy:  "reject"|"audit"|"allow",
  nestedExtglobPolicy:    "reject"|"audit"|"allow",
  inputKind:              "regex"|"glob",
  maxBoundedRepeat:       number,
  maxConsecutiveStars:    number,
  maxPatternBytes:        number,
}

Build a b.gateContract gate that screens ctx.identifier (or ctx.pattern) before any compilation step. Action chain: serve (no issues) → audit-only (warn-only) → refuse (any critical or high). No sanitize action — pattern strings cannot be repaired. Compose into framework parsers / form validators / route matchers so operator-fed patterns hit the guard before reaching new RegExp().

var gate = b.guardRegex.gate({ profile: "strict" });

gate.check({ identifier: "(a+)+b" }).then(function (rv) {
  rv.ok;                                           // → false
  rv.action;                                       // → "refuse"
});

gate.check({ identifier: "^[a-z]+$" }).then(function (rv) {
  rv.action;                                       // → "serve"
});

b.guardRegex.assertSafe(input, label?, ErrorClass?, code?, opts?) #

stable0.15.39
{
  profile:             string,   // guardRegex profile (default: "strict")
  boundedRepeatPolicy: string,   // default: "allow" (large bounded repeats are linear)
}

Screen an already-compiled RegExp (or a raw pattern string) for catastrophic-backtracking (ReDoS) shapes, throwing if the pattern is unsafe. This is the config-time guard for request-lifecycle code that matches an operator-supplied regex against attacker-controlled input (User-Agent, Origin, request path, form field, HELO) — an accidentally-catastrophic operator pattern would otherwise be a per-request DoS once a hostile input triggers the backtracking.

Pass a RegExp instance (its .source is screened) or a pattern string. On a hostile shape it throws ErrorClass(code, ...) when an error class is supplied, otherwise the underlying GuardRegexError. Returns the input unchanged on success.

By default it rejects the catastrophic-backtracking classes — nested, alternation-with, and lookaround quantifiers — but ALLOWS large/open bounded repeats ({8,}, {n,m}): a single counted repeat is linear, not exponential, and legitimate patterns (e.g. a hex hash of 8+ digits) use them. Pass an explicit opts to override.

b.guardRegex.assertSafe(/^[a-z]+$/);            // ok — returns the RegExp
b.guardRegex.assertSafe(/\.[a-f0-9]{8,}\./);    // ok — a single bounded repeat is linear
try { b.guardRegex.assertSafe(/((a)+)+$/); }    // throws — nested quantifier
catch (e) { e.code; }                           // → "regex/unsafe-pattern"

b.guardRegex.compliancePosture(name) #

stable0.7.13hipaapci-dssgdprsoc2

Look up a compliance-posture overlay by name (one of "hipaa" / "pci-dss" / "gdpr" / "soc2"). Returns a fresh clone of the posture overlay so the caller may mutate it freely without disturbing the shared table. Throws GuardRegexError with code "regex.bad-posture" when the name is not one this guard maps. Wired by gateContract.defineGuard through gateContract.lookupCompliancePosture, so the clone semantics and error code are identical across every guard in the family.

var posture = b.guardRegex.compliancePosture("hipaa");
posture;                                             // → overlay clone (mutable)

try {
  b.guardRegex.compliancePosture("not-a-regime");
} catch (e) {
  e.code;                                            // → "regex.bad-posture"
}

b.guardRegex.buildProfile(opts) #

stable0.7.13
{
  extends:   string|string[],   // base profile name(s) to compose
  ...:       any guard key,      // inline override of resolved keys
}

Compose a derived profile from one or more named bases plus inline overrides, resolving names through this guard's own PROFILES table. opts.extends is a base profile name ("strict" / "balanced" / "permissive") or an array of names — later entries shadow earlier ones, and inline opts keys win last. Wired by gateContract.defineGuard through gateContract.makeProfileBuilder, so operator-defined profiles stay traceable to a baseline instead of a hand-typed dictionary.

var custom = b.guardRegex.buildProfile({ extends: "strict" });
custom;                                              // → composed profile object

b.guardRegex.loadRulePack(pack) #

stable0.7.13

Register an operator-supplied rule pack with this guard's rule-pack registry. The pack is identified by pack.id (a non-empty string) and stored for later dispatch by gates that opt in via opts.rulePackId. Returns the pack unchanged on success; throws GuardRegexError with code "regex.bad-opt" when pack is missing or pack.id is not a non-empty string. Wired by gateContract.defineGuard through gateContract.makeRulePackLoader, so storage shape and validation are identical across the family.

var pack = b.guardRegex.loadRulePack({ id: "tenant-policy", rules: [] });
pack.id;                                             // → "tenant-policy"

b.guardRegex.resolveOpts(opts?) #

stable0.7.13
{
  profile:           string,    // one of PROFILES; default this guard's default
  compliancePosture: string,    // overlay one of hipaa/pci-dss/gdpr/soc2
}

Resolve caller opts against this guard's PROFILES + compliance-posture overlays into the fully-defaulted option set the guard runs on — the same resolution validate / sanitize / gate apply internally. Wired by gateContract.defineGuard from the guard's binding config (profiles / postures / defaults / error class), so a guard's bespoke gate calls resolveOpts instead of re-declaring the per-guard resolver wrapper. Throws GuardRegexError with code "regex.bad-opt" / "regex.bad-posture" on an unknown profile or posture name.

var resolved = b.guardRegex.resolveOpts({ profile: "strict" });
resolved.profile;                                    // → "strict"

Last updated 2026-08-08T16:39:49.652Z by seeder.