A privacy filter scans outgoing JSON payloads for strings that look like identifying filesystem paths. The current check is a regex written for ordinary text and runs over the serialized document. A Windows style path inside a JSON string is serialized with doubled backslashes, and can also appear with unicode escapes or forward slashes, so the pattern written for raw text does not match the escaped form and the value passes through. Known evidence: the mismatch is a decoding layer problem, not a weak pattern problem, since the regex is applied to the encoded representation rather than to the decoded string values. Open questions: the cleanest way to walk a parsed JSON tree and validate each decoded string value with normalization for separators, drive letters, home directory markers and UNC prefixes; how to report a rejection with only structural metadata such as key path, rule id and a hash or length, so the rejected content itself is never written to logs; and how to unit test the filter with synthetic non identifying fixtures so that reasoning and executed evidence stay distinct.
Validating JSON string values for filesystem paths when escaping defeats plain-text regex
Resolution from reasoning only; no tests were executed. The root cause is a layer mismatch: a rule authored against decoded text is applied to the serialized document, where every literal backslash is doubled and other characters may be unicode escaped. A pattern of the form drive letter, colon, separator, non separator consumes the first backslash of the pair and then sees the second, so it fails on every backslash separated path in escaped form. Fix at the layer, not the pattern: parse the document, walk the tree, apply detection to each decoded string value, and let the outbound path transmit only the canonical reserialization of the validated tree. Normalize before matching with a bounded fixed point: at most two decoding rounds for nested JSON, percent encoding and unicode escapes, collapse mixed separators, and treat a stripped prefix such as a network share, file scheme or home marker as a forced match rather than an empty residue. Detect on structural signals: drive letter root, share root, rooted separator, home marker followed by segments, and two or more segments joined by separators. Rejection records carry only structural metadata: rule id, JSON pointer to the field built from schema known keys, decoded length, and a keyed HMAC of the value with a secret held outside the log store so an operator can confirm a suspected false positive by recomputing but cannot invert. Never include parser error messages, which quote input, and never include unknown key text. Fail closed: the verdict starts at deny and only a successful validation that returns canonical bytes flips it. Test plan with synthetic fixtures using reserved drive letters and placeholder segments: escaped backslash path rejected, unicode escaped separator rejected, nested JSON in a string rejected, prefix only value rejected, benign text with doubled backslashes in a code sample not falsely flagged, and log output asserted to contain no substring of any fixture. These assertions are stated, not run. No new skill: the three matched skills already cover decoded value validation, normalization stage leaks and value free telemetry.
Extension for resource exhaustion: deep or huge documents and backtracking prone matcher input. Reasoning only, no executed tests. Budgets belong in one ledger threaded through parse, walk, normalize and match, shared across the whole document so work cannot be split across many medium values. Bytes are checked on raw length before parsing. Depth is counted by an event parser and an iterative walker with an explicit stack, because a recursive parser overflows the native stack before any depth check runs. Nodes bound wide documents. Matcher work is charged as decoded length times rule count times decoding round, with a linear engine or single pass scanner; a backtracking engine gets a length cap and relies on the deadline as backstop only. The two senses of failing closed are separated by worker isolation: the sharing path spawns the filter as a separate worker with its own memory and stack limits, the parent holds the verdict at deny before spawning, and only zero exit plus complete canonical bytes flips it to allow. Nonzero exit, signal death or deadline expiry leaves deny and records one of three codes: worker crash, worker out of memory, worker deadline. The primary task is unaffected because its output is committed before the sharing job is enqueued and it never reads the verdict. One in flight worker with a busy code bounds concurrency. Warning cardinality rule named first detailed then counted: one detailed record per code and field pointer pair per fixed window, then counters only; codes are a compile time enumeration, pointers are schema known keys with unknown keys reported under an unknown key code on the parent pointer, so series count is the product of two closed sets; a per window flood cap adds at most one record; lengths are bucketed into powers of two; parser error text is never included. Untested assertions: a generated hundred thousand level document leaves the parent alive with verdict deny; an oversized document shows zero parse work; a megabyte of one repeated character completes within the linear bound; every log line matches a closed grammar with no fixture substrings.