reported via email on 4 June 2026 - no response.
The CVE-2026-33894 fix (af094e6) is incomplete. It only checks the top-level DigestInfo element count:
lib/rsa.js:1174 if(!asn1.validate(obj, digestInfoValidator, capture, errors) || obj.value.length !== 2) { ... }
The nested DigestAlgorithm SEQUENCE has no element-count check, and asn1.validate ignores extra children. An attacker can stuff garbage bytes inside DigestAlgorithm; node-forge still accepts the DigestInfo and captures the attacker's digest. Because _parseAllDigestBytes defaults to true, trailing garbage is blocked, but garbage inside the structure is not.
I confirmed the parser gap on 1.4.0 (a DigestAlgorithm with [OID, NULL, garbage] validates, top-level length stays 2, attacker digest captured), and then forged a working signature end to end on an e=3 key with no private key:
node-forge version: 1.4.0
pub.verify(forged) => true (message "forged-by-attacker-no-privkey")
control (forged sig vs different message) => false
The forgery uses the standard low-exponent technique: the 32-byte digest is fixed at the bottom of the block, the ASN.1/padding framing at the top, and ~314 free "garbage" bytes inside DigestAlgorithm absorb the middle of S^3 so that S^3 is an exact cube matching a block node-forge accepts.
Suggested fix: require the DigestAlgorithm AlgorithmIdentifier SEQUENCE to contain exactly the OID plus optional NULL (add a nested value.length check), or make asn1.validate reject unconsumed children. Validating element counts at every nesting level (not just the outer SEQUENCE) closes the remaining slack.
reported via email on 4 June 2026 - no response.
The CVE-2026-33894 fix (af094e6) is incomplete. It only checks the top-level DigestInfo element count:
lib/rsa.js:1174 if(!asn1.validate(obj, digestInfoValidator, capture, errors) || obj.value.length !== 2) { ... }
The nested DigestAlgorithm SEQUENCE has no element-count check, and asn1.validate ignores extra children. An attacker can stuff garbage bytes inside DigestAlgorithm; node-forge still accepts the DigestInfo and captures the attacker's digest. Because _parseAllDigestBytes defaults to true, trailing garbage is blocked, but garbage inside the structure is not.
I confirmed the parser gap on 1.4.0 (a DigestAlgorithm with [OID, NULL, garbage] validates, top-level length stays 2, attacker digest captured), and then forged a working signature end to end on an e=3 key with no private key:
node-forge version: 1.4.0
pub.verify(forged) => true (message "forged-by-attacker-no-privkey")
control (forged sig vs different message) => false
The forgery uses the standard low-exponent technique: the 32-byte digest is fixed at the bottom of the block, the ASN.1/padding framing at the top, and ~314 free "garbage" bytes inside DigestAlgorithm absorb the middle of S^3 so that S^3 is an exact cube matching a block node-forge accepts.
Suggested fix: require the DigestAlgorithm AlgorithmIdentifier SEQUENCE to contain exactly the OID plus optional NULL (add a nested value.length check), or make asn1.validate reject unconsumed children. Validating element counts at every nesting level (not just the outer SEQUENCE) closes the remaining slack.