Nov 2, 2026·10 min read·1 visit
Versions 0.19.0 and earlier of @sd-jwt/core and @sd-jwt/decode fail to reject invalid UTF-8 byte sequences in base64url disclosures and do not verify that all presented disclosures match an Issuer-signed _sd digest. Attackers can corrupt specific disclosures to strip restrictive claims from a credential presentation while still passing cryptographic signature verification.
CVE-2026-62374 (GHSA-f9j6-8p6x-r9j6) is a verification bypass vulnerability in the OpenWallet Foundation SD JWT JS library (@sd-jwt/core and @sd-jwt/decode versions 0.19.0 and earlier). Due to permissive UTF-8 decoding and the omission of mandatory reference checks during presentation verification, tampered or unreferenced disclosures are silently dropped rather than rejected. This deviates from RFC 9901 §7.1 requirements and allows attackers to execute silent claim-set reduction attacks against relying parties.
SD JWT JS (sd-jwt-js) is an open-source TypeScript and JavaScript implementation of the IETF Selective Disclosure for JSON Web Tokens (SD-JWT, RFC 9901) specification, maintained by the OpenWallet Foundation. The library provides cryptographic primitives and parsing routines for issuing, holding, presenting, and verifying selective disclosure credentials in decentralized identity ecosystems. Prior to version 0.20.0, the functionality was split across multiple modular npm packages, primarily @sd-jwt/core and @sd-jwt/decode.
In an SD-JWT architecture, an Issuer signs a JWT payload containing cryptographic hash digests (_sd array) corresponding to individual cleartext claims formatted as separate base64url-encoded Disclosure segments. When a Holder presents the credential to a Relying Party (verifier), the verifier parses the wire-format string (<Issuer-signed-JWT>~<Disclosure 1>~...~<Disclosure N>~<KB-JWT>), computes the hash of each presented disclosure, matches the computed hashes against the signed _sd digests in the JWT payload, and reconstructs the verified claim set.
CVE-2026-62374 (tracked in the GitHub Advisory Database as GHSA-f9j6-8p6x-r9j6) stems from two compounding RFC 9901 compliance failures in @sd-jwt/decode and @sd-jwt/core versions 0.19.0 and earlier. First, the disclosure parser performs lossy UTF-8 decoding (CWE-176) rather than rejecting malformed byte sequences. Second, the claim reconstruction logic fails to verify that every disclosure supplied in the presentation matches a valid digest in the Issuer-signed JWT (CWE-754).
As a result of these flaws, when a presentation contains a corrupted, tampered, or unreferenced disclosure, the verification routine does not halt or throw an exception. Instead, it silently discards the unmatched disclosure and returns a valid verification result containing a reduced subset of claims. This enables a silent claim-set reduction (downgrade) attack against relying parties.
The root cause of CVE-2026-62374 lies in how the library processes incoming base64url-encoded disclosure segments and maps them back to the signed JWT structure. According to RFC 9901 §4.2.1 and §7.1 step 3, every disclosure is a base64url-encoded UTF-8 string representing a JSON array ([salt, claim_name, claim_value] for object properties or [salt, claim_value] for array elements). If any disclosure cannot be decoded as valid base64url or parsed into a valid UTF-8 JSON array, the verifier must reject the entire SD-JWT presentation.
In vulnerable versions of @sd-jwt/decode and @sd-jwt/core, base64url decoding relied on standard Node.js Buffer.toString('utf-8') or a default JavaScript TextDecoder instance initialized without the { fatal: true } option. When encountering invalid UTF-8 byte sequences—such as an isolated continuation byte or an invalid lead byte like 0xF2—these decoders perform lossy character replacement by substituting the invalid byte with the Unicode replacement character U+FFFD (\uFFFD). If the byte modification occurs within a string literal (such as the salt or claim value), the resulting string remains syntactically valid JSON and passes JSON.parse().
Once parsed, the library hashes the raw disclosure to map it against the _sd digest list in the Issuer-signed JWT. Because the underlying bytes of the disclosure were altered (either intentionally by an attacker or via lossy decoding), the newly computed digest does not match the authentic digest signed by the Issuer. Under RFC 9901 §7.1 step 5, if any provided disclosure is not referenced by a digest value in the Issuer-signed JWT (directly or recursively), the verifier is strictly required to reject the SD-JWT.
However, the unpacking routines (unpackObj and unpackObjInternal) inverted this verification logic. Rather than iterating over the presented disclosures to ensure each one was consumed by an _sd digest, the algorithm iterated exclusively over the _sd arrays inside the signed JWT payload and looked up matching entries in the decoded disclosure map. Any disclosure in the map whose digest did not match an entry in _sd remained unvisited and was silently ignored upon function return.
The remediation was implemented in commit 3d8a72a0a279d67439db038c9c2b52621568b866 (PR #380) and released in @sd-jwt/core version 0.20.0, which also consolidated the standalone @sd-jwt/decode package into @sd-jwt/core. The patch addresses both the permissive UTF-8 decoding and the missing unreferenced disclosure checks.
First, to remediate CWE-176, the maintainers introduced a strict decoding utility in packages/core/src/utils/strict-json.ts. By instantiating TextDecoder with { fatal: true }, any malformed UTF-8 byte sequence immediately triggers a TypeError, which is caught and rethrown as an SDJWTException:
// packages/core/src/utils/strict-json.ts
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
export const decodeBase64urlJsonStrict = <T>(
encoded: string,
errorMessage: string,
): T => {
try {
const bytes = base64UrlToUint8Array(encoded);
// Throws if bytes contain invalid UTF-8 sequences instead of inserting U+FFFD
const decoded = utf8Decoder.decode(bytes);
return JSON.parse(decoded) as T;
} catch {
throw new SDJWTException(errorMessage);
}
};In packages/core/src/utils/disclosure.ts, the Disclosure.fromEncode and Disclosure.fromEncodeSync methods were updated to replace the permissive JSON.parse(base64urlDecode(s)) call with decodeBase64urlJsonStrict:
// packages/core/src/utils/disclosure.ts
- const item = JSON.parse(base64urlDecode(s)) as DisclosureData<T>;
+ const item = decodeBase64urlJsonStrict<DisclosureData<T>>(
+ s,
+ 'Invalid disclosure data',
+ );Second, to remediate CWE-754 and enforce RFC 9901 §7.1 step 5, packages/core/src/decode/decode.ts was updated to track every digest matched during the recursive traversal of the JWT payload. In unpackObj, a seenDigests set is passed into unpackObjInternal. After traversal completes, the function verifies that every digest present in the incoming disclosure map was referenced at least once in seenDigests:
// packages/core/src/decode/decode.ts
export const unpackObj = (obj: unknown, map: Record<string, Disclosure>) => {
const copiedObj = JSON.parse(JSON.stringify(obj));
const seenDigests = new Set<string>();
const result = unpackObjInternal(copiedObj, map, '', seenDigests);
// RFC 9901 Section 7.1 step 5: reject unreferenced disclosures
const mapDigests = Object.keys(map);
const unusedDigests = mapDigests.filter((d) => !seenDigests.has(d));
if (unusedDigests.length > 0) {
throw new SDJWTException('Unreferenced disclosure(s) detected in SD-JWT');
}
return result;
};The patch also hardened adjacent validation paths in decode.ts by adding validateReservedFields() to reject disclosures attempting to overwrite reserved SD-JWT keys (_sd, _sd_alg, _sd_decoy) and updating getSDAlgAndPayload() to verify _sd_alg against an allowlist of IANA-registered hash algorithms (IANA_HASH_ALGORITHMS). The combination of strict byte-level validation and complete digest accounting provides a complete fix against both byte-corruption and arbitrary digest mismatch variants.
CVE-2026-62374 was discovered via differential fuzzing against RFC 9901 reference implementations (sd-jwt in Python and the RFC 9901 specification oracle). When fed a credential presentation containing 5 disclosures where a single disclosure (family_name) was mutated by injecting the invalid UTF-8 lead byte 0xF2, the Python reference implementations rejected the token (UnicodeDecodeError / R_DISCLOSURE_DECODE), whereas @sd-jwt/decode 0.19.0 accepted the presentation and returned a verified payload silently omitting family_name.
Exploitation can occur under two primary threat models: a malicious credential Holder or an on-path network attacker. In the malicious Holder scenario, the attacker possesses a valid Issuer-signed SD-JWT that includes negative or restrictive claims (such as is_sanctioned: true, account_suspended: true, or has_driving_restrictions: true). While in standard SD-JWT a holder can omit a disclosure by removing it from the ~-separated string, certain application protocols or state channels may inspect the number of disclosures, or a proxy/wallet layer may automatically attach all disclosures before transmission.
More critically, if an on-path attacker intercepts an SD-JWT presentation in transit where Holder Key Binding (KB-JWT) is not enforced by the Relying Party, the intermediary cannot forge the Issuer's signature, nor do they need to know which specific claim corresponds to which _sd digest. By flipping a single byte in a targeted disclosure segment (or injecting 0xF2), the attacker invalidates that disclosure's hash without invalidating the overall presentation structure. Even when Key Binding is enforced, a malicious Holder who controls the holder private key can intentionally corrupt a restrictive disclosure and sign a valid KB-JWT (sd_hash) over the corrupted presentation.
The following unit test demonstrates the exact exploitation mechanics against SDJwtInstance:
test('rejects tampered disclosure bytes', async () => {
const { signer, verifier } = createSignerVerifier();
const sdjwt = new SDJwtInstance<SdJwtPayload>({
signer,
signAlg: 'EdDSA',
verifier,
hasher: digest,
hashAlg: 'sha-256',
saltGenerator: generateSalt,
});
const claims = {
firstname: 'John',
lastname: 'Doe',
};
const disclosureFrame: DisclosureFrame<typeof claims> = {
_sd: ['firstname', 'lastname'],
};
const encodedSdjwt = await sdjwt.issue(claims, disclosureFrame);
const parts = encodedSdjwt.split('~');
// Tamper with the first disclosure by injecting an invalid UTF-8 lead byte (0xF2)
const disclosureBytes = Buffer.from(parts[1], 'base64url');
disclosureBytes[2] = 0xf2;
parts[1] = Buffer.from(disclosureBytes).toString('base64url');
const tamperedSdjwt = parts.join('~');
// In <= 0.19.0: sdjwt.verify(tamperedSdjwt) resolves to { lastname: 'Doe' }
// In >= 0.20.0: rejects with SDJWTException('Invalid disclosure data')
await expect(sdjwt.verify(tamperedSdjwt)).rejects.toThrow(
'Invalid disclosure data',
);
});The primary security impact of CVE-2026-62374 is high integrity loss (VI:H in CVSS v4.0) affecting Relying Party verification pipelines. Cryptographic verification functions (verify() and decodeSdJwt()) are designed to provide an atomic guarantee: either the presentation conforms strictly to the Issuer's signed digests and RFC 9901 wire formatting, or verification fails entirely.
When this guarantee is broken, downstream authorization engines that rely on the output of sdjwt.verify() are exposed to authorization bypasses. Specifically, if a Relying Party implements a default-allow policy—granting access unless a specific restrictive claim or revocation flag is present in the returned claim object—silently dropping the tampered disclosure allows an unauthorized entity to bypass security controls.
Furthermore, differential parsing behavior across heterogeneous microservice architectures introduces state desynchronization risks. If an API gateway or edge proxy written in Node.js (@sd-jwt/core <= 0.19.0) validates an incoming SD-JWT presentation and forwards either the raw token or the parsed claims to a backend service written in Python (sd-jwt) or Go, the two systems will reach conflicting security verdicts on the exact same cryptographic input.
CVE-2026-62374 carries a CVSS v4.0 base score of 7.1 (CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N). The vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog and has an EPSS score of 0.00226 (13.29th percentile), reflecting that while proof-of-concept test cases exist in public repositories, no active wild exploitation has been observed.
Organizations and developers using the OpenWallet Foundation SD JWT JS libraries must upgrade @sd-jwt/core to version 0.20.0 or later. Version 0.20.0 enforces strict fatal UTF-8 decoding on all base64url segments and rejects presentations containing unreferenced or duplicate disclosure digests.
> [!NOTE]
> As part of the version 0.20.0 release, the sd-jwt-js monorepo underwent architectural consolidation. The standalone packages @sd-jwt/decode, @sd-jwt/present, @sd-jwt/utils, and @sd-jwt/types were merged directly into @sd-jwt/core. Consequently, no version 0.20.0 patch exists for @sd-jwt/decode on the npm registry.
To remediate projects that depend directly on @sd-jwt/decode, remove @sd-jwt/decode from package.json, install @sd-jwt/core@^0.20.0, and update all module import statements to reference @sd-jwt/core:
npm uninstall @sd-jwt/decode
npm install @sd-jwt/core@^0.20.0In addition to upgrading the library, Relying Party implementations should implement defense-in-depth measures within their verification logic:
KB-JWT): Require Holder Key Binding on all SD-JWT presentations so that the sd_hash claim inside the signed KB-JWT cryptographically binds the exact sequence of disclosures, preventing on-path network intermediaries from mutating disclosure bytes in transit.CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
@sd-jwt/core OpenWallet Foundation | <= 0.19.0 | 0.20.0 |
@sd-jwt/decode OpenWallet Foundation | <= 0.19.0 | Consolidated into @sd-jwt/core 0.20.0 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-62374 (GHSA-f9j6-8p6x-r9j6) |
| Primary CWE | CWE-754 (Improper Check for Unusual Conditions) / CWE-176 (Improper Handling of Unicode Encoding) |
| CVSS v4.0 Score | 7.1 High (CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N) |
| Attack Vector | Network (Tampered SD-JWT Disclosure Segment) |
| Impact | Silent Claim-Set Reduction / Verification Integrity Bypass |
| Affected Packages | @sd-jwt/core <= 0.19.0, @sd-jwt/decode <= 0.19.0 |
| Patched Version | @sd-jwt/core 0.20.0 |
| Exploit & KEV Status | PoC Available / Not Listed in CISA KEV (EPSS: 0.23%) |
The software does not check or improperly checks for unusual or exceptional conditions that are not expected to occur during normal operation, such as unreferenced selective disclosure digests and invalid UTF-8 byte sequences.
An interpretation conflict in Bytebase DBHub (< 0.22.6) allows attackers to bypass read-only SQL query restrictions when connected to MySQL or MariaDB instances. Because DBHub's custom SQL tokenizer treats any '--' sequence as a comment, whereas MySQL and MariaDB require trailing whitespace or a control character, crafted queries containing '--' immediately followed by non-whitespace characters allow stacked destructive SQL statements to execute on connections configured as read-only.
CVE-2026-118777 is a prototype pollution vulnerability (CWE-1321) in the Forge JavaScript cryptography and networking library (distributed on npm as node-forge) prior to version 1.4.1. The flaw resides in the forge.http client subsystem, where incoming HTTP cookie names and paths parsed from Set-Cookie response headers are stored directly into standard JavaScript objects without key validation or property isolation. By returning crafted Set-Cookie headers containing keys such as __proto__ or constructor, a malicious HTTP server can inject arbitrary properties into Object.prototype, attach attributes to the global Object constructor, or delete built-in prototype methods to cause an application-wide denial of service.
CVE-2026-118775 (GHSA-4642-5mcj-2rqw) is an integer overflow and validation flaw in the ASN.1 serialization module of Forge (node-forge) prior to version 1.4.1. The asn1.oidToDer() function fails to validate the range of the first two arcs of an Object Identifier (OID) and truncates the packed initial subidentifier to a single 8-bit integer. Applications that perform policy checks on untrusted OID strings before encoding them into Distinguished Encoding Rules (DER) format can be tricked into emitting DER bytes that correspond to a completely different, restricted OID.
CVE-2026-119194 is an algorithmic complexity vulnerability (CWE-407) in the pyasn1 library for Python, affecting versions 0.3.3 through 0.6.4. The flaw resides in the schemaless decoding routine ConstructedPayloadDecoderBase._decodeComponentsSchemaless used across BER, CER, and DER codecs. By sending an ASN.1 container populated with child components whose tag identifiers are separated by multiples of CPython's integer hash modulus (sys.hash_info.modulus), an unauthenticated attacker can force worst-case hash collisions in an unbounded Python set, degrading decoding performance from O(N) to O(N^2) and causing a Denial of Service (DoS).
CVE-2026-119195 is an algorithmic complexity vulnerability (CWE-407) in the pyasn1 library for Python, affecting all versions prior to 0.6.5. When decoding constructed strings or indefinite-length ANY types across BER, CER, and certain DER code paths, the payload decoders repeatedly concatenate incoming fragments onto an immutable bytes object using the += operator. Because Python bytes objects cannot be resized in-place, each fragment iteration allocates a new buffer and copies all previously accumulated bytes, resulting in quadratic O(N^2) time and memory copy complexity. An unauthenticated remote attacker can supply a highly fragmented ASN.1 stream to exhaust CPU cycles and stall application worker threads.
An algorithmic complexity vulnerability (CWE-407) in pyasn1 versions prior to 0.6.5 allows unauthenticated attackers to cause a Denial of Service (DoS) via crafted ASN.1 payloads. The flaw resides in the binary-encoded ASN.1 REAL mantissa deserialization routine within RealPayloadDecoder.valueDecoder. By performing iterative byte-string slicing and repeated bitwise shifts on a growing arbitrary-precision integer, the decoder exhibits quadratic time and memory allocation complexity relative to the mantissa length. Supplying a BER-, CER-, or DER-encoded payload with a large REAL mantissa causes excessive CPU consumption and blocks application worker processes.