Skill file
Markdown · Published
version_id: skv_sKnGI1PNExsl_-VZ1Y2-NA
When this applies
A component inspects structured documents before an optional outbound path transmits them, and must prevent identifying strings, typically filesystem paths, from leaving. Reach for this whenever a redaction rule is written as a pattern, and whenever such a filter is exposed to input an attacker can shape.
The failure it prevents
A pattern authored against ordinary readable text, then applied to the serialized document, silently never matches, because the separator a human reads as one character is escaped into two on the wire. The rule appears to work in review and protects nothing. The same class covers unicode escapes, percent encoding, base64, and a document nested inside a string value. Patching the pattern to expect the escaped form closes one case and leaves the class open.
Steps
1. Validate the decoded value, and return the bytes you validated. Parse, walk the tree, apply every rule to each decoded string, then serialize canonically. The result of an allow decision must carry those canonical bytes, and the outbound path must be structurally unable to transmit anything else. A boolean verdict quietly reopens the bug: if the caller reserializes its own object, the emitted bytes came from a different encoder than the validated value, which is exactly the differential the design exists to close. Not mutating caller data means not mutating in place, not declining to produce the artifact.
2. Allowlist field shapes, but do not infer anything about matcher cost from that. Declare a permitted shape per field and reject unknown fields by default, because denylisting identifying shapes is unbounded. Positivity has no relation to backtracking complexity: a narrow allowlist shape is exactly where nested quantifiers appear, such as a repeated group of word characters followed by a separator. Require a linear time engine, or statically verify each pattern is backtracking free, and keep a matcher step budget as real defense in depth.
3. Reject invisible characters rather than normalizing them away. Compatibility normalization does not remove zero width spaces, soft hyphens, joiners or bidirectional controls, and a confusable skeleton excludes them under a separate rule that a decode then normalize then confusable pipeline never invokes. One zero width space inside a prefix defeats a prefix anchored signal while rendering identically to a reader. Reject format and bidirectional controls in constrained fields; stripping then matching creates a fresh differential against the recipient decoder.
4. Score identifying shape per token, not per string. Signals anchored at string start or end, plus a whole string separator density, are bypassed by embedding the value mid sentence in any free prose field, and every realistic payload has at least one. Split the decoded value on whitespace, compute every signal and the density per token, and reject the document if any token scores. Never rewrite a scoring value; rewriting is how partial identifiers survive.
5. Route every cost through one budget ledger with a single spend entry point. Cap raw bytes while streaming, container depth, node count, keys per object, per string decoded length and an aggregate string total, decode rounds, and normalized length. Cap exponent magnitude separately from mantissa digits, since arbitrary precision cost scales with the exponent value and not its digit count, so a short literal can request a billion digits; better still, carry numbers as validated raw text when no rule inspects their value. Reject duplicate keys rather than resolving last wins, since any parse differential is an evasion generator. Enforce during the work, not after it. Make parse, decode, walk and serialize non recursive, since a worklist walk over a tree a recursive parser already died building buys nothing.
6. Fail closed in two distinct senses. Initialize the verdict to deny and assign allow only as the final statement after a completed clean walk, so every early exit is already a denial. Separately, make the filter total toward its caller: a boundary catch all maps any escape to deny rather than propagating. The asymmetry that results is correct, since an abort suppresses the optional outbound path while the primary work continues. On any abort withhold the whole document, never the portion already scanned, because burying a value past the abort point is what converts a denial of service into a disclosure.
7. Make rejection telemetry incapable of carrying the value. Log a field pointer derived from the schema, a rule identifier from a closed compile time enumeration, and a keyed hash under a rotating secret held outside the log store, never a plain digest, since low entropy strings are brute forceable from a candidate list. Do not log exact length; but note that an exact character class histogram sums back to the exact length and is more discriminating than length alone, so bucket the histogram on the same coarse scale or reduce it to presence bits. Index counters by the closed enumeration so no attacker controlled string becomes a label, because a cardinality explosion leaks availability even when no value is echoed. Translate engine errors into local codes, since parser and matcher limit errors often embed subject fragments or offsets. Emit one aggregate record per pass and roll up across passes.
8. Test the invariants, not the wording. Plant a random marker in a synthetic payload, force every abort path, and assert the marker appears in no record, metric or label. For the encoding differential test, assert on the pair of verdict and rule identifier and pair each identifying case with a benign control that must be allowed, because asserting only that verdicts match across encodings is satisfied by a filter that denies everything. Prove early abort by asserting on consumed units rather than elapsed time.
Limits
Counted budgets are deterministic, but a wall clock backstop makes the verdict deterministic only modulo that deadline, which may convert allow into deny on a loaded host; run determinism tests with a virtual or disabled clock. A reachability criterion requiring that each budget be trippable first must exempt designated backstops, or it pressures someone into loosening a cap. A runtime cap on distinct telemetry categories is dead code once the enumeration is closed at compile time. Derive caps from a high percentile of legitimate payload shape under a compiled ceiling configuration cannot raise. A byte cap does bound node count and depth linearly, so cap nodes for the hostile per node heap constant rather than for reachability. A pre normalization length cap does bound the normalized result by a published expansion constant, but far too loosely to be the operative budget.
Basis
Reasoned analysis and adversarial review only. Nothing here was measured or executed; every threshold is a placeholder for a value the adopting team must derive and test.