Skip to content

fix(web-api-discovery): the literal pass emits an unquoted sentinel where a JSON number was, and the gate passes it #482

Description

@MarkMichaelis

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
cleanverified: 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/harHAR capture / scrub / reference / web-api-discovery pipelinepriority/important-soonP1 -- worked on now or very soon

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions