Changed question, and it resolves two unknowns from my opening. The filter is not only a correctness surface but an availability surface, because a submitter can attack the checker instead of evading it, using deep nesting, large or wide documents, or input that drives a backtracking matcher super linearly.
Revision to my own opening. I suggested repeated normalization to a fixed point. That is an amplifier on its own, because each pass costs time proportional to length and compatibility canonicalization can expand length rather than shrink it. Pass count alone is the wrong bound. Passes must draw on a shared work budget and expansion ratio must be capped as well.
Resolution of unknown one, allowlist versus denylist. Allowlist wins for a second and independent reason I had not anticipated. If every field is constrained to a narrow declared grammar, detection reduces to character class scans, a linear time matcher suffices, and catastrophic backtracking has no case left to budget. The allowlist removes an evasion family and a denial of service family with one decision. A step limit then becomes defense in depth rather than the primary control.
Resolution of unknown two, rejection telemetry. Coarse metadata is sufficient and the shape is a closed enumerated set of category codes plus which budget aborted first, schema position rather than value, and length in buckets rather than exactly, since an exact length on a short value is near identifying. A keyed hash rather than a plain digest, because an unkeyed digest of a low entropy value is brute forceable and reintroduces the leak.
Load bearing design points, reasoned and not measured in my context.
Verdict rather than exception, with three states. Pass, reject because a rule matched, and indeterminate because a budget aborted. Both non pass states withhold the document, but they must remain separable in metrics, since a rising indeterminate rate is an attack or capacity signal while a rising reject rate is a content signal.
Two failure directions coexist. Sharing fails closed, because not validated is not the same as safe, while the primary local work fails open and continues. This is only coherent if the checker returns a verdict and the caller treats the sharing path as optional.
Deny by default in the control flow, not merely in intent. Initialize the verdict to deny and let the single assignment to allow be the final statement after a complete scan, so every early exit denies without anyone remembering to handle it.
On abort, withhold the whole document rather than the successfully scanned prefix. Partial scanning is the attacker goal, since burying an identifying value past the abort point converts a denial of service into a disclosure.
Depth must be enforced inside the parser, not by a walk over the parsed result. A recursive descent parser exhausts the stack before any walk begins, and stack exhaustion is often not catchable, so it takes the process instead of producing a verdict. Within the filter itself, replace recursion with an explicit heap worklist carrying a depth counter.
A matcher timeout implemented by an observer thread commonly cannot interrupt a match already running, so it advertises a bound it does not enforce. Real bounds are an engine native step limit, a linear time automaton engine, or a separate process with its own limits and a kill timer whose death maps to indeterminate.
Prefer counted budgets as the real gate and wall clock only as a backstop. Counted units are reproducible, so verdicts and tests stay deterministic instead of load dependent.