The defect
applyLiteralPass runs over the serialized whole-HAR string
(sanitize-har.js:661), replacing each profile literal with an angle-bracket
sentinel by plain text substitution. It has no idea what JSON context the value
sat in.
When the literal was a quoted string, the result is well-formed:
"userId":"<literal>" -> "userId":"<FacebookUserId>" valid
When the literal was a bare JSON number, it is not:
"productIdentifier":<literal>} -> "productIdentifier":<FacebookUserId>} INVALID
An angle-bracket sentinel is not a JSON token, so the document no longer parses.
Evidence
Real capture, MarkMichaelis/CodiwomplerSocialMedia,
.har-captures/www.facebook.com/2026-09-10-124423, entry 322:
raw span replaced : 14 chars (first char: a digit)
scrub span written : 16 chars (first char: "<")
raw char just BEFORE : ":"
raw char just AFTER span : "}"
scrubbed window : "…tID":null,"productIdentifier":<FacebookUserId>}"
The value was unquoted — a JSON number — and the sentinel replaced it in place.
Relationship to #479
#479 is the same symptom (a request payload rewritten into invalid JSON) via a
different mechanism. The fix for #479 decodes percent-encoded form bodies before
detection and replacement, which takes that capture from 15 of 24 unparseable
payloads to 1 of 24. This issue is the remaining 1.
They are separate because they live in different modules and neither fix implies
the other: #479 is pii.js reading escapes as digits; this is har-literals.js
ignoring JSON context. #479 should stay open until both land.
Why the leak gate does not catch it
Same blind spot as #479 and #475: verify-scrub asks whether a secret survived,
never whether the document it produced is still well-formed. This capture scrubbed
clean — verified: true, errors: 0 — with an unparseable payload in it.
Suggested fix
The literal pass already has the machinery: har-literals.js carries
percentDecode, parseJsonObject, decodeNestedJson and
transformEncodedParams, so it is not naive about encoding — only about JSON
value context.
The narrow fix is to quote the sentinel when the literal it replaces is not
already delimited: if the character before the match is : or , or [ and the
match is not preceded by ", emit "<Sentinel>" rather than <Sentinel>.
The more robust fix is the one #479 took — replace structurally on the parsed
document rather than textually on the serialization — but the literal pass runs
last, deliberately, over everything including non-JSON bodies, so that change is
larger than it looks and should be weighed rather than assumed.
Test note
The fixture must contain the literal as a bare JSON number, not a quoted
string. A string-valued fixture produces a well-formed result today and would keep
doing so after any fix, falsifying nothing.
Related
#479 (same symptom, percent-escape mechanism, fixed separately), #475 (a value
the scrubber's own table says to replace, surviving), #480 (a name-only secret in
a nested payload, never detected). All four share a root shape: the run holds
what it needs to check its own work and does not. Queue: #368.
The defect
applyLiteralPassruns over the serialized whole-HAR string(
sanitize-har.js:661), replacing each profile literal with an angle-bracketsentinel by plain text substitution. It has no idea what JSON context the value
sat in.
When the literal was a quoted string, the result is well-formed:
When the literal was a bare JSON number, it is not:
An angle-bracket sentinel is not a JSON token, so the document no longer parses.
Evidence
Real capture,
MarkMichaelis/CodiwomplerSocialMedia,.har-captures/www.facebook.com/2026-09-10-124423, entry 322:The value was unquoted — a JSON number — and the sentinel replaced it in place.
Relationship to #479
#479 is the same symptom (a request payload rewritten into invalid JSON) via a
different mechanism. The fix for #479 decodes percent-encoded form bodies before
detection and replacement, which takes that capture from 15 of 24 unparseable
payloads to 1 of 24. This issue is the remaining 1.
They are separate because they live in different modules and neither fix implies
the other: #479 is
pii.jsreading escapes as digits; this ishar-literals.jsignoring JSON context. #479 should stay open until both land.
Why the leak gate does not catch it
Same blind spot as #479 and #475:
verify-scrubasks whether a secret survived,never whether the document it produced is still well-formed. This capture scrubbed
clean —
verified: true,errors: 0— with an unparseable payload in it.Suggested fix
The literal pass already has the machinery:
har-literals.jscarriespercentDecode,parseJsonObject,decodeNestedJsonandtransformEncodedParams, so it is not naive about encoding — only about JSONvalue context.
The narrow fix is to quote the sentinel when the literal it replaces is not
already delimited: if the character before the match is
:or,or[and thematch is not preceded by
", emit"<Sentinel>"rather than<Sentinel>.The more robust fix is the one #479 took — replace structurally on the parsed
document rather than textually on the serialization — but the literal pass runs
last, deliberately, over everything including non-JSON bodies, so that change is
larger than it looks and should be weighed rather than assumed.
Test note
The fixture must contain the literal as a bare JSON number, not a quoted
string. A string-valued fixture produces a well-formed result today and would keep
doing so after any fix, falsifying nothing.
Related
#479 (same symptom, percent-escape mechanism, fixed separately), #475 (a value
the scrubber's own table says to replace, surviving), #480 (a name-only secret in
a nested payload, never detected). All four share a root shape: the run holds
what it needs to check its own work and does not. Queue: #368.