Every CVE Search/ Get API key

CVE-2026-62374

SD JWT JS: Tampered disclosures silently dropped instead of rejected

7.1 HighFix available

SD JWT JS is a JavaScript implementation of the Selective Disclosure JWT (SD-JWT) specification. In version 0.19.0 and earlier, @sd-jwt/decode, also used by @sd-jwt/core, silently drops a tampered Disclosure whose digest no longer matches the Issuer-signed _sd array and returns a reduced claim set, reporting success while the Issuer signature remains valid. Both a tampered Disclosure that remains valid JSON and a Disclosure containing invalid UTF-8 bytes reach this silent-drop behavior. In the latter case, the TextDecoder / Buffer decoding path substitutes U+FFFD replacement characters, recovering syntactically valid disclosure data instead of rejecting the malformed input. This violates RFC 9901 section 7.1 step 5, which requires rejection of unreferenced Disclosures, and section 7.1 step 3 / section 4.2.1, which require rejection of invalid Disclosures. A malicious holder can exploit this omission primitive, including through SDJwtInstance.verify() when key binding is required. An on-path attacker can also tamper with a Disclosure when the relying party does not enforce key binding; enforced key binding rejects on-path tampering because it invalidates the KB-JWT sd_hash binding. If the relying party defaults to allowing access when a constraining age, role, or restriction claim is absent, the silent claim reduction can change an authorization decision without an error. This issue is fixed in @sd-jwt/core version 0.20.0; no patched version of the standalone @sd-jwt/decode package is identified.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
Low
User interaction
None

What an attacker can affect

Confidentiality
None
Integrity
High
Availability
None

Exploit likelihood

0.23%Unchanged since Oct 30, 2026
90%50%10%1%

What each source reports

CVE List Score used 7.1

SD JWT JS: Tampered disclosures silently dropped instead of rejected

SD JWT JS is a JavaScript implementation of the Selective Disclosure JWT (SD-JWT) specification. In version 0.19.0 and earlier, @sd-jwt/decode, also used by @sd-jwt/core, silently drops a tampered Disclosure whose digest no longer matches the Issuer-signed _sd array and returns a reduced claim set, reporting success while the Issuer signature remains valid. Both a tampered Disclosure that remains valid JSON and a Disclosure containing invalid UTF-8 bytes reach this silent-drop behavior. In the latter case, the TextDecoder / Buffer decoding path substitutes U+FFFD replacement characters, recovering syntactically valid disclosure data instead of rejecting the malformed input. This violates RFC 9901 section 7.1 step 5, which requires rejection of unreferenced Disclosures, and section 7.1 step 3 / section 4.2.1, which require rejection of invalid Disclosures. A malicious holder can exploit this omission primitive, including through SDJwtInstance.verify() when key binding is required. An on-path attacker can also tamper with a Disclosure when the relying party does not enforce key binding; enforced key binding rejects on-path tampering because it invalidates the KB-JWT sd_hash binding. If the relying party defaults to allowing access when a constraining age, role, or restriction claim is absent, the silent claim reduction can change an authorization decision without an error. This issue is fixed in @sd-jwt/core version 0.20.0; no patched version of the standalone @sd-jwt/decode package is identified.

Record
CVE-2026-62374
CVSS 4.0
7.1 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-754, CWE-176
Updated
2026-11-04
EUVDEUVD-2026-113215 7.1

SD JWT JS is a JavaScript implementation of the Selective Disclosure JWT (SD-JWT) specification. In version 0.19.0 and earlier, @sd-jwt/decode, also used by @sd-jwt/core, silently drops a tampered Disclosure whose digest no longer matches the Issuer-signed _sd array and returns a reduced claim set, reporting success while the Issuer signature remains valid. Both a tampered Disclosure that remains valid JSON and a Disclosure containing invalid UTF-8 bytes reach this silent-drop behavior. In the latter case, the TextDecoder / Buffer decoding path substitutes U+FFFD replacement characters, recovering syntactically valid disclosure data instead of rejecting the malformed input. This violates RFC 9901 section 7.1 step 5, which requires rejection of unreferenced Disclosures, and section 7.1 step 3 / section 4.2.1, which require rejection of invalid Disclosures. A malicious holder can exploit this omission primitive, including through SDJwtInstance.verify() when key binding is required. An on-path attacker can also tamper with a Disclosure when the relying party does not enforce key binding; enforced key binding rejects on-path tampering because it invalidates the KB-JWT sd_hash binding. If the relying party defaults to allowing access when a constraining age, role, or restriction claim is absent, the silent claim reduction can change an authorization decision without an error. This issue is fixed in @sd-jwt/core version 0.20.0; no patched version of the standalone @sd-jwt/decode package is identified.

Record
EUVD-2026-113215
CVSS 4.0
7.1 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Updated
2026-11-04
GitHub AdvisoriesGHSA-f9j6-8p6x-r9j6 7.1

SD JWT JS: Tampered disclosures silently dropped instead of rejected

Summary

When one Disclosure in an SD-JWT presentation is tampered so its digest no longer matches the Issuer-signed _sd array, @sd-jwt/decode 0.19.0 does not reject the presentation. It silently drops the unmatched Disclosure and returns a reduced claim set, reporting success. The Issuer signature is untouched and valid.

This is a downgrade/omission primitive. A malicious or curious holder can present a tampered Disclosure and have the relying party receive fewer claims than the credential carries, with no error surfaced. Where the relying party does not enforce key binding, an in-transit (on-path) attacker can achieve the same by mangling one Disclosure's bytes; where key binding is enforced, on-path tampering instead breaks the holder's KB-JWT sd_hash binding (so it is rejected), and the downgrade is then exercisable by the key-bound holder — confirmed end-to-end with KB required.

Two distinct tampering inputs converge on the same silent-drop sink:

  1. Genuinely non-decodable Disclosure (broken base64url -> bytes that are not valid UTF-8, e.g. an injected 0xF2 lead byte). RFC 9901 §7.1 step 3 / §4.2.1 require rejection. @sd-jwt/decode instead lossy-decodes it: the TextDecoder/Buffer path substitutes U+FFFD (REPLACEMENT CHARACTER) for the invalid byte and recovers a syntactically valid disclosure (the salt gains a U+FFFD, claim family_name = "Smith"). The recovered Disclosure then hashes to a digest absent from the signed _sd array, so it is silently skipped.
  2. Tampered-but-still-valid-JSON Disclosure (digest no longer matches _sd): same end state — the unmatched Disclosure is silently skipped.

Two RFC 9901 MUSTs compound:

  • §7.1 step 5 (primary): "If any Disclosure was not referenced by digest value in the Issuer-signed JWT (directly or recursively via other Disclosures), the SD-JWT MUST be rejected." — instead silently dropped.
  • §7.1 step 3 / §4.2.1 (secondary): a non-base64url-JSON Disclosure MUST be rejected — the lenient lossy decode skips this check.

Confirmed end-to-end through SDJwtInstance.verify() with a real ES256 issuer key, and again with key binding REQUIRED (a valid holder ES256 KB-JWT over aud+nonce+sd_hash).

Impact

Silent claim-set reduction is a downgrade/omission primitive. If a relying party's authorization logic is "default-allow when a constraining claim is absent" (an age / role / restriction attribute), dropping that one Disclosure flips the decision, with no error surfaced. The decoder reporting success on a tampered presentation is the core problem; tampering should be a hard reject. Because SD-JWT VC and the EU EUDIW ARF profiles reuse this disclosure layer, the exposure is to wallet/verifier deployments built on @sd-jwt/*.

Proof of concept (minimized — NOT a weaponized exploit)

A clean single-mutation credential: lowercase _sd_alg: "sha-256", an honest Issuer body whose _sd digests are computed over the valid wire Disclosures, and exactly one wire byte-segment tampered — the family_name Disclosure made genuinely non-decodable (an invalid-UTF-8 0xF2 byte injected into its base64url). Everything else is byte-for-byte valid.

The byte-exact credential cannot be faithfully pasted as text (it deliberately contains the invalid 0xF2 byte that triggers the lossy decode). It is provided as bug3_clean_tampered.cred with an untampered twin bug3_clean_control.cred, plus the minting script mint_bug3_clean.py and a three-oracle harness run_three_oracles.py — shared privately with the maintainer on first reply. Structure: 5 Disclosures, only index 1 (family_name) tampered; all other segments byte-for-byte valid.

Observed on the byte-identical credential (three independent oracles):

| Oracle | Version | Verdict | Detail | |---|---|---|---| | RFC 9901 reference (pure Python) | reference impl | REJECT | R_DISCLOSURE_DECODE | | sd-jwt (Daniel Fett's…

Record
GHSA-f9j6-8p6x-r9j6
CVSS 4.0
7.1 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-176
Updated
2026-11-02
NVD 7.1

SD JWT JS is a JavaScript implementation of the Selective Disclosure JWT (SD-JWT) specification. In version 0.19.0 and earlier, @sd-jwt/decode, also used by @sd-jwt/core, silently drops a tampered Disclosure whose digest no longer matches the Issuer-signed _sd array and returns a reduced claim set, reporting success while the Issuer signature remains valid. Both a tampered Disclosure that remains valid JSON and a Disclosure containing invalid UTF-8 bytes reach this silent-drop behavior. In the latter case, the TextDecoder / Buffer decoding path substitutes U+FFFD replacement characters, recovering syntactically valid disclosure data instead of rejecting the malformed input. This violates RFC 9901 section 7.1 step 5, which requires rejection of unreferenced Disclosures, and section 7.1 step 3 / section 4.2.1, which require rejection of invalid Disclosures. A malicious holder can exploit this omission primitive, including through SDJwtInstance.verify() when key binding is required. An on-path attacker can also tamper with a Disclosure when the relying party does not enforce key binding; enforced key binding rejects on-path tampering because it invalidates the KB-JWT sd_hash binding. If the relying party defaults to allowing access when a constraining age, role, or restriction claim is absent, the silent claim reduction can change an authorization decision without an error. This issue is fixed in @sd-jwt/core version 0.20.0; no patched version of the standalone @sd-jwt/decode package is identified.

Record
CVE-2026-62374
CVSS 4.0
7.1 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Weakness
CWE-176, CWE-754
Updated
2026-11-04
OSV 7.1

SD JWT JS: Tampered disclosures silently dropped instead of rejected

SD JWT JS is a JavaScript implementation of the Selective Disclosure JWT (SD-JWT) specification. In version 0.19.0 and earlier, @sd-jwt/decode, also used by @sd-jwt/core, silently drops a tampered Disclosure whose digest no longer matches the Issuer-signed _sd array and returns a reduced claim set, reporting success while the Issuer signature remains valid. Both a tampered Disclosure that remains valid JSON and a Disclosure containing invalid UTF-8 bytes reach this silent-drop behavior. In the latter case, the TextDecoder / Buffer decoding path substitutes U+FFFD replacement characters, recovering syntactically valid disclosure data instead of rejecting the malformed input. This violates RFC 9901 section 7.1 step 5, which requires rejection of unreferenced Disclosures, and section 7.1 step 3 / section 4.2.1, which require rejection of invalid Disclosures. A malicious holder can exploit this omission primitive, including through SDJwtInstance.verify() when key binding is required. An on-path attacker can also tamper with a Disclosure when the relying party does not enforce key binding; enforced key binding rejects on-path tampering because it invalidates the KB-JWT sd_hash binding. If the relying party defaults to allowing access when a constraining age, role, or restriction claim is absent, the silent claim reduction can change an authorization decision without an error. This issue is fixed in @sd-jwt/core version 0.20.0; no patched version of the standalone @sd-jwt/decode package is identified.

Record
CVE-2026-62374
CVSS 4.0
7.1 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-176, CWE-754
Updated
2026-11-07
OSVGHSA-f9j6-8p6x-r9j6 7.1

SD JWT JS: Tampered disclosures silently dropped instead of rejected

Summary

When one Disclosure in an SD-JWT presentation is tampered so its digest no longer matches the Issuer-signed _sd array, @sd-jwt/decode 0.19.0 does not reject the presentation. It silently drops the unmatched Disclosure and returns a reduced claim set, reporting success. The Issuer signature is untouched and valid.

This is a downgrade/omission primitive. A malicious or curious holder can present a tampered Disclosure and have the relying party receive fewer claims than the credential carries, with no error surfaced. Where the relying party does not enforce key binding, an in-transit (on-path) attacker can achieve the same by mangling one Disclosure's bytes; where key binding is enforced, on-path tampering instead breaks the holder's KB-JWT sd_hash binding (so it is rejected), and the downgrade is then exercisable by the key-bound holder — confirmed end-to-end with KB required.

Two distinct tampering inputs converge on the same silent-drop sink:

  1. Genuinely non-decodable Disclosure (broken base64url -> bytes that are not valid UTF-8, e.g. an injected 0xF2 lead byte). RFC 9901 §7.1 step 3 / §4.2.1 require rejection. @sd-jwt/decode instead lossy-decodes it: the TextDecoder/Buffer path substitutes U+FFFD (REPLACEMENT CHARACTER) for the invalid byte and recovers a syntactically valid disclosure (the salt gains a U+FFFD, claim family_name = "Smith"). The recovered Disclosure then hashes to a digest absent from the signed _sd array, so it is silently skipped.
  2. Tampered-but-still-valid-JSON Disclosure (digest no longer matches _sd): same end state — the unmatched Disclosure is silently skipped.

Two RFC 9901 MUSTs compound:

  • §7.1 step 5 (primary): "If any Disclosure was not referenced by digest value in the Issuer-signed JWT (directly or recursively via other Disclosures), the SD-JWT MUST be rejected." — instead silently dropped.
  • §7.1 step 3 / §4.2.1 (secondary): a non-base64url-JSON Disclosure MUST be rejected — the lenient lossy decode skips this check.

Confirmed end-to-end through SDJwtInstance.verify() with a real ES256 issuer key, and again with key binding REQUIRED (a valid holder ES256 KB-JWT over aud+nonce+sd_hash).

Impact

Silent claim-set reduction is a downgrade/omission primitive. If a relying party's authorization logic is "default-allow when a constraining claim is absent" (an age / role / restriction attribute), dropping that one Disclosure flips the decision, with no error surfaced. The decoder reporting success on a tampered presentation is the core problem; tampering should be a hard reject. Because SD-JWT VC and the EU EUDIW ARF profiles reuse this disclosure layer, the exposure is to wallet/verifier deployments built on @sd-jwt/*.

Proof of concept (minimized — NOT a weaponized exploit)

A clean single-mutation credential: lowercase _sd_alg: "sha-256", an honest Issuer body whose _sd digests are computed over the valid wire Disclosures, and exactly one wire byte-segment tampered — the family_name Disclosure made genuinely non-decodable (an invalid-UTF-8 0xF2 byte injected into its base64url). Everything else is byte-for-byte valid.

The byte-exact credential cannot be faithfully pasted as text (it deliberately contains the invalid 0xF2 byte that triggers the lossy decode). It is provided as bug3_clean_tampered.cred with an untampered twin bug3_clean_control.cred, plus the minting script mint_bug3_clean.py and a three-oracle harness run_three_oracles.py — shared privately with the maintainer on first reply. Structure: 5 Disclosures, only index 1 (family_name) tampered; all other segments byte-for-byte valid.

Observed on the byte-identical credential (three independent oracles):

| Oracle | Version | Verdict | Detail | |---|---|---|---| | RFC 9901 reference (pure Python) | reference impl | REJECT | R_DISCLOSURE_DECODE | | sd-jwt (Daniel Fett's…

Record
GHSA-f9j6-8p6x-r9j6
CVSS 4.0
7.1 HighCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Weakness
CWE-176
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
@sd-jwt/decode
npm
through 0.19.0 None known
@sd-jwt/core
npm
through 0.19.0
0.20.0
@sd-jwt/core
npm
before 0.20.0
0.20.0

History

History
RecordedChangedSources
facts, referencesCVE List
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-62374 is a high vulnerability (CVSS 7.1) in Openwallet-foundation @sd-jwt/core, published October 29, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.23%, the 13th percentile. Fixed in @sd-jwt/core 0.20.0.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open