# SD JWT JS: Tampered disclosures silently dropped instead of rejected

@sd-jwt/decode silently drops tampered SD-JWT disclosures instead of rejecting, letting a holder or on-path attacker reduce the claim set a verifier sees.

- Payout: Research
- Severity: high
- Program: sd-jwt-js
- Found by: tyche-dev
- Disclosed: 2026-11-02
- CVE: CVE-2026-62374
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-f9j6-8p6x-r9j6
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-f9j6-8p6x-r9j6
- Tags: area:oss domain:oss impact:integrity kind:research platform:ghsa sev:high tech:jwt technique:fuzzing technique:parser-differential technique:unicode-normalization vuln:access-control vuln:auth-bypass vuln:business-logic

**Target:** npm @sd-jwt/decode / @sd-jwt/core SD-JWT disclosure decode layer
**Root cause:** Lossy UTF-8 decode (TextDecoder/Buffer substituting U+FFFD) recovers a syntactically valid disclosure from invalid bytes, and disclosures whose digest is absent from the signed _sd array are silently skipped rather than causing rejection per RFC 9901 §7.1 step 5.
**Steps:**
1. Mint a valid SD-JWT with 5 disclosures and an honest issuer _sd digest array.
2. Inject an invalid-UTF-8 byte `0xF2` into the base64url of the `family_name` disclosure (index 1), leaving all other segments byte-for-byte valid.
3. Pass the credential to `SDJwtInstance.verify()` in @sd-jwt/decode 0.19.0.
4. Observe ACCEPT with reduced claim set `{given_name, email, vct, iss}` — `family_name` silently dropped, no error.
5. Compare against RFC 9901 reference and pip `sd-jwt` 0.10.4, which both REJECT (`R_DISCLOSURE_DECODE` / `UnicodeDecodeError`).
**How found:** Differential fuzzing of the SD-JWT disclosure layer against an RFC 9901 reference oracle and two reference libraries; the divergence (ACCEPT vs REJECT on the same byte-identical credential) surfaced the lossy-decode and silent-skip behavior.
**Exploitation:** A malicious holder (or, without key binding, an on-path attacker mangling one disclosure's bytes) presents a tampered disclosure; the verifier's decoder reports success with the constraining claim omitted. If the RP's authorization logic defaults to allow when a claim is absent (age/role/restriction), the decision flips. With key binding enforced, on-path tampering breaks the KB-JWT sd_hash and is rejected, so the downgrade is then exercisable by the key-bound holder.
**Escalation:** Chained with default-allow authorization logic on the relying party to flip access decisions; affects SD-JWT VC and EU EUDIW ARF wallet/verifier deployments reusing @sd-jwt/*.
**Insight:** When auditing JWT/SD-JWT decoders, feed disclosures with invalid UTF-8 lead bytes (e.g. 0xF2) and check whether the library lossy-decodes to U+FFFD instead of throwing — a recovered-but-unmatched disclosure that gets silently skipped is a claim-omission primitive, not just a parse quirk.

