Normalization-stage leaks in decoded-value path filters: prefixes, home markers, decoders, warnings
Review checklist for an egress filter that already validates decoded JSON strings. Covers seven ways the normalization and diagnostics stages still leak or miss identifying paths: prefix-only values, real home expansion, undefined decoders, parser error text, field identifiers in budget warnings, round ordering, and re-serializing the wrong tree.
Normalization-stage leaks in decoded-value path filters
Trigger
Use this after a filter for outgoing structured documents has already moved from matching serialized bytes to matching decoded string values, and now applies a normalization pass before a path detector. The checks here are the ones an adversarial review found in a design that had already adopted decoded-value matching, a shared work ledger, and value-free diagnostics. Each one is easy to miss because the surrounding design looks correct.
The failure it prevents
The normalization stage is written to make matching robust, but each transformation is also a place where the identifying value can be erased before detection, manufactured by the filter itself, or echoed through a side channel the diagnostics design forgot. The filter passes review and either lets a host or share name leave, or leaks the rejected value through its own error handling.
Checks
- A stripped prefix must force a match, not clear the way. If the normalizer removes a network-share prefix, a file URI scheme, or a home shorthand, a value consisting of only that prefix normalizes to almost nothing, fails every structural signal, and is allowed. The share or host name then leaves in the original bytes. Fix: any stripped prefix sets a path-shaped flag that counts as a content match regardless of what remains.
- Never expand a home shorthand to the real home directory. Doing so makes the filter create the identifying string in its own memory, and that string then feeds the keyed hash and the character-class summary. Fix: expand to a fixed marker token that is the same on every host.
- Name the decoders, or the convergence check is vacuous. A rule that says reject when the value is still decodable after the last round means nothing unless the rounds actually decode something. List the decoders explicitly: percent decoding, unicode escape decoding, and a nested document escape inside a string. Define decodable as a decoder changed the string on the final round. Without this, a percent-encoded or doubly serialized path is the original defect one layer down.
- Run invisible-character removal and confusable folding first, and require at least two rounds. If removal runs last, a zero-width character between a drive letter and its colon survives prefix detection on the first pass. Include fullwidth and lookalike separators in the folding map. Strip only in the match-only copy, never in the emitted value, so no differential against the recipient decoder is created.
- Normalization is match-only; re-serialize the original decoded tree. State this explicitly. Sending the normalized tree silently mutates content. Sending a tree that was never validated reopens the encoding differential. The emitted bytes come from the same decoded tree that was walked, before any match-only transformation.
- Discard parser and matcher error text entirely. Standard parsers embed a context snippet in their message. If that text reaches the typed outcome, the log, or an exception, the parse category leaks the value. Map every engine error to a local code and drop the message.
- Budget-exhaustion warnings carry no field identifier. The field at which depth, size, or work ran out is partial scan state and reveals document structure to whoever reads the log. Size, depth, work, and parse categories carry only the category and a coarse budget-fraction bucket. Only a content-match category carries a field identifier, a rule identifier, and a keyed hash. Duplicate or unknown keys use a fixed placeholder identifier, never the key text, because a key can itself be a path.
Supporting rules
- Recognize non-file URI schemes before separator folding, or every ordinary link in free text becomes path-shaped and is rejected.
- Enforce the per-string length cap once, in the tokenizer, as a reject. A truncating cap in the walk leaves the full string in the tree for re-serialization.
- Set the output size cap with escaping expansion in mind. Canonical escaping can expand a single byte to six, so an output cap equal to the input cap is either dead code or rejects for an unrelated reason.
- Keep diagnostics local, with a host-local rotating key that is never logged, and include the rule and field identifiers in the hash input so the same value in two fields does not link.
- Route warnings through a sink that strips ambient hostname and user fields the logger would otherwise append.
Test fixtures the original plan lacked
Generate in process and assert only on category and fixture index. Include: each escaped-separator encoding; a prefix-only value; a zero-width character inside a prefix; a nested serialized document; a percent-encoded path; a key that is a path; a document under every per-dimension cap but over the shared ledger; a parser that throws with a context snippet; a logger that throws; and benign controls such as URLs and dates that must be allowed. Assert that no fixture substring of at least four characters appears in any encoding of the log output, the typed outcome, or a caught exception.
Limits
Support is reasoned analysis and one independent adversarial review. Nothing was executed, measured, or inspected in a running system. Thresholds such as round count and cap ratios are placeholders. The stance on stripping invisible characters applies only to a match-only copy; for constrained fields with a positive grammar, rejecting them outright remains the safer rule.