Skip to content

👷 Update dependency node-forge to v1.4.1 [SECURITY] - #5221

Merged
thomas-lebeau merged 1 commit into
mainfrom
renovate/npm-node-forge-vulnerability
Nov 2, 2026
Merged

thomas-lebeau merged 1 commit into
mainfrom
renovate/npm-node-forge-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Adoption Passing Confidence
node-forge 1.4.0 → 1.4.1 age adoption passing confidence

Warning

Some dependencies could not be looked up. Check the Dependency Dashboard for more information.


Forge: X.509 basicConstraints bypass in verifyCertificateChain() via deprecated extension OIDs 2.5.29.10 and 2.5.29.13

CVE-2026-119190 / GHSA-5x38-2vff-x55p

More information

Details

Summary

node-forge parses certificate extensions with the deprecated OIDs 2.5.29.10 and 2.5.29.13 as basicConstraints, and forge.pki.verifyCertificateChain() used the first extension with that name. A certificate with a forged deprecated-OID extension (cA TRUE) before its genuine basicConstraints extension (cA FALSE) is accepted as a CA certificate, and the same technique can hide a pathLenConstraint. Fixed in node-forge 1.4.1.

Details

lib/oids.js maps the deprecated certificate extension OIDs 2.5.29.10 and 2.5.29.13 to the name basicConstraints, the same name as the RFC 5280 extension 2.5.29.19. certificateExtensionFromAsn1() in lib/x509.js decodes extensions by name, so these extensions are decoded into the same cA and pathLenConstraint fields. cert.getExtension() returns the first extension with a matching name, and pki.verifyCertificateChain() looked up basicConstraints by name.

As a result, a certificate that contains a 2.5.29.10 (or 2.5.29.13) extension with cA TRUE before its genuine basicConstraints extension with cA FALSE is accepted as a CA certificate. The same technique can hide a CA certificate's pathLenConstraint. A deprecated-OID extension on its own also satisfies the check added for GHSA-2328-f5f3-gj25 (CVE-2026-33896) that basicConstraints is present. RFC 5280 section 4.2.1.9 defines basicConstraints only as id-ce 19 (2.5.29.19), and the path validation checks in section 6.1.4 apply to that extension.

The fix makes chain verification look up basicConstraints by OID (cert.getExtension({id: '2.5.29.19'})). This covers both the cA and the pathLenConstraint checks. Certificate parsing, the OID table and cert.getExtension('basicConstraints') are unchanged.

Patches

Fixed in node-forge 1.4.1. Chain verification now uses only the basicConstraints extension with OID 2.5.29.19.

Workarounds

None in node-forge itself. Applications that pass an options.verify callback to pki.verifyCertificateChain() (or verify to forge.tls.createConnection()) can reject chains in which a non-leaf certificate has no 2.5.29.19 extension with cA TRUE, for example with cert.getExtension({id: '2.5.29.19'}). CA operators can avoid copying unrecognized extensions from certificate requests.

Credits

WorkOS Security Team· security@workos.com (@​nickcollisson-workos)

Proof of concept

Have a trusted CA issue an end-entity certificate whose extensions are, in this order: a 2.5.29.10 (or 2.5.29.13) extension with the value of a basicConstraints cA TRUE (DER 30 03 01 01 FF), then the CA's own 2.5.29.19 basicConstraints with cA FALSE, and no keyUsage. For example, openssl x509 -req -copy_extensions copy with an extensions file that sets basicConstraints = CA:FALSE copies the extension from the CSR and produces this order. Use that certificate's key to sign a leaf certificate for any name.

On node-forge <= 1.4.0, forge.pki.verifyCertificateChain(caStore, [leaf, eeCert]) accepts the chain, and eeCert.getExtension('basicConstraints') returns the 2.5.29.10 extension with cA true. The same certificate without the deprecated-OID extension is rejected ("Certificate basicConstraints indicates the certificate is not a CA."). A certificate with only the deprecated-OID extension is also accepted.

The fix adds regression tests in tests/unit/x509.js and the reporter's PoC as tests/pocs/ghsa-5x38-2vff-x55p.js.

Impact

Applications that verify certificate chains with forge.pki.verifyCertificateChain(), including TLS peer certificate verification in forge.tls, are affected.

Exploitation requires a CA trusted by the application to issue a certificate that contains the attacker-supplied deprecated-OID extension, typically a CA that copies extensions from certificate signing requests. For example, openssl x509 -req -copy_extensions copy (or copyall) with an extensions file that sets basicConstraints = CA:FALSE and no keyUsage produces such a certificate. If the issuing CA adds a keyUsage extension without keyCertSign, node-forge rejects the chain.

The holder of such a certificate can then issue certificates for any name that pki.verifyCertificateChain() accepts. In forge's own TLS stack this allows impersonating servers (or clients, where client certificates are verified) and intercepting traffic.

Severity

  • CVSS Score: 7.4 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Forge: Denial of service in TLS record processing via call stack exhaustion

CVE-2026-119187 / GHSA-hqq3-7jhf-c5jx

More information

Details

Summary

Connections created with forge.tls.createConnection() processed received TLS records recursively, one stack level per record. A peer can send one chunk of thousands of small records, such as heartbeat records, which are accepted before the handshake, and process() throws RangeError: Maximum call stack size exceeded, which terminates Node.js processes that use the documented socket wiring. Fixed in node-forge 1.4.1.

Details

In lib/tls.js, c.process() reads one record and passes it to _update(), which calls the handler for the record type from ctTable. Every handler (tls.handleHeartbeat, tls.handleApplicationData, tls.handleHandshake for fragmented messages, tls.handleHelloRequest and the other handshake handlers) ends by calling c.process() again to continue with the next buffered record, or the next message in the same record. Each record therefore adds process → _update → handler to the call stack. In Node.js 24, about 2,300 eight-byte records (under 20 KB) in one chunk are enough for process() to throw RangeError: Maximum call stack size exceeded.

ctTable maps heartbeat records to tls.handleHeartbeat in every client and server state, so heartbeat records are processed before the handshake without any extension negotiation. The attack needs no authentication and no valid handshake messages. Both server connections (attacked by a client) and client connections (attacked by the server they connect to) are affected. Other record types recurse the same way: a handshake message fragmented over many records, many HelloRequest messages in one record, or many application data records after the handshake.

node-forge reports TLS protocol errors through the error callback, so applications do not normally wrap process() in try/catch. When process() is called from a Node.js socket data handler, as in the documented usage, the exception is uncaught and terminates the process. If the exception is caught, the connection is left partially processed and should be discarded.

No specification is violated; this is an implementation flaw (uncontrolled recursion).

The fix makes c.process() iterative. The record-reading code moves into _processRecord(), and c.process() runs it in a loop. A handler's call to c.process() without new data, made while records are being processed, now only flags that the loop should continue. Calls with new data are processed right away as before, so callback order and return values are unchanged.

Patches

Fixed in node-forge 1.4.1. Records are now processed in a loop, so the stack depth no longer depends on the number of records.

Workarounds

Wrap every call to connection.process() in try/catch, and on an exception close the connection (connection.close()) and destroy the underlying socket. This prevents the process crash but not the loss of the attacked connection.

Credits

Reported jointly by Ziyue Wang, Chenchen, Strick Sheng, Liyi Zhou, and Maurice Ng.

Proof of concept

Input: one chunk of N identical heartbeat records, each 8 bytes:

18 03 01 00 03 01 00 00
(content type 24 = heartbeat, TLS 1.0, length 3, heartbeat_request with a 0-byte payload)

// c = forge.tls.createConnection({server: true, ...})
var rec = forge.util.hexToBytes('1803010003010000');
c.process(new Array(5001).join(rec)); // 5,000 records, 40,000 bytes

On 1.4.0 (Node.js 24), process() throws RangeError: Maximum call stack size exceeded after about 2,300 records, before any handshake. The same happens for a client connection. In a Node.js server that calls c.process(data.toString('binary')) from socket.on('data', ...), a client sending these 40,000 bytes terminates the server process. On 1.4.1 all records are processed and the server keeps running.

Regression tests in tests/unit/tls.js, "record processing": 50,000 heartbeat records in one call in server and client mode, a handshake message fragmented over 50,000 records, many HelloRequest messages per record, and checks that byte-by-byte input and exceptions from callbacks behave as before.

Impact

Applications that use node-forge's TLS implementation: connections from forge.tls.createConnection(), including those created by forge.tls.wrapSocket() and the forge.http / forge.xhr TLS connections, which call process() on received data. Any peer that can connect to a forge TLS server, or any server a forge TLS client connects to, can send a small chunk of records (under 20 KB) without authentication. In Node.js applications using the documented usage, the uncaught exception terminates the process, which drops every connection it serves; repeated chunks keep it down. In browsers the impact is limited to the attacked connection. There is no confidentiality or integrity impact.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Forge: Denial of service in TLS record processing via a record interleaved with a fragmented handshake message

CVE-2026-119188 / GHSA-8hrf-8g6f-7xwp

More information

Details

Summary

A connection created with forge.tls.createConnection() throws an uncaught TypeError from process() when it receives a record of another content type while waiting for the rest of a handshake message that was split across records. A peer can trigger this with 18 bytes before the handshake, without authentication, which terminates Node.js processes that use the documented socket wiring. Fixed in node-forge 1.4.1.

Details

In lib/tls.js, _readRecord() handles a record that arrives while a handshake message split across records is incomplete. If the record has any other content type (change_cipher_spec, alert, application_data or heartbeat), forge correctly reports "Invalid fragmented record." through c.error() with a fatal unexpected_message alert. That fatal error closes and resets the connection, which sets c.record to null. _readRecord() then continues with c.record.ready = true and throws TypeError: Cannot set properties of null (setting 'ready') out of process(). The code is unchanged since the first release.

RFC 5246 section 6.2.1 allows a handshake message to be fragmented across records, and RFC 8446 section 5.1 forbids interleaving handshake messages with other record types. Failing the connection with unexpected_message is the intended behavior; the bug is the crash after the error.

node-forge reports TLS protocol errors through the error callback, so applications do not normally wrap process() in try/catch. When process() is called from a Node.js socket data handler, as in the documented usage, the exception is uncaught and terminates the process.

The fix returns from _readRecord() right after the fatal error. The connection is failed, the alert is sent and the error callback is called as before, and process() no longer throws.

Patches

Fixed in node-forge 1.4.1. The connection still fails with a fatal unexpected_message alert and the error callback, but process() no longer throws.

Workarounds

Wrap every call to connection.process() in try/catch. The connection has already been failed and closed when the exception is thrown, so the exception can be ignored or the socket destroyed.

Proof of concept

Input (server mode), two records, 18 bytes:

16 03 01 00 06 01 00 00 10 03 01 handshake record: ClientHello header announcing 16 bytes, 2 present
15 03 01 00 02 02 28 alert record

// c = forge.tls.createConnection({server: true, ...})
c.process(forge.util.hexToBytes('1603010006010000100301' + '15030100020228'));

On 1.4.0 the error callback receives "Invalid fragmented record.", then process() throws TypeError: Cannot set properties of null (setting 'ready'). A change_cipher_spec, application_data or heartbeat record in place of the alert has the same effect, and a client connection behaves the same with a fragment of a ServerHello. In a Node.js server that calls c.process(data.toString('binary')) from socket.on('data', ...), a client sending these 18 bytes terminates the server process. On 1.4.1 the connection fails with a fatal unexpected_message alert and process() returns normally.

Regression tests in tests/unit/tls.js, "fragmented handshake messages": each of the four other record types after a handshake fragment, in server and client mode, plus same-type continuation records that are still accepted.

Impact

Applications that use node-forge's TLS implementation: connections from forge.tls.createConnection(), including those created by forge.tls.wrapSocket() and the forge.http / forge.xhr TLS connections. Any peer that can connect to a forge TLS server, or any server a forge TLS client connects to, can send 18 bytes before the handshake, without authentication. In Node.js applications using the documented usage, the uncaught exception terminates the process, which drops every connection it serves, and the attacker can repeat it on every restart. There is no confidentiality or integrity impact.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Forge: PKCS#12 MAC forgery and constant-key PBE via zero iteration count

CVE-2026-118782 / GHSA-jc66-7q78-782g

More information

Details

Summary

forge.pkcs12.pkcs12FromAsn1() accepts a PKCS#12 MAC iteration count of zero (or less) from the input, and the PKCS#12 key derivation function then returns a constant key that does not depend on the password or salt. An attacker who does not know the password can modify a PKCS#12/PFX file and compute a MAC that verifies with any password; PKCS#12 PBE-encrypted private keys prepared the same way decrypt with any password. Fixed in node-forge 1.4.1.

Details

p12.pkcs12FromAsn1() in lib/pkcs12.js reads the MAC iteration count (MacData.iterations) from the input and passes it to the PKCS#12 key derivation function pki.pbe.generatePkcs12Key() in lib/pbe.js (also exposed as forge.pkcs12.generateKey()) without checking that it is at least 1. The key derivation function (RFC 7292 Appendix B.2) runs its hash loop iter times. With an iteration count of 0 (or an empty or negative INTEGER), no hashing is done and the function returns the PKCS#12 diversifier ID byte repeated, for example 0x03 repeated 20 times for an HMAC-SHA1 MAC key. The result does not depend on the password or the salt, so anyone can compute a valid MAC over arbitrary PFX contents.

The same key derivation function is used by pki.pbe.getCipherForPKCS12PBE() for the PKCS#12 password-based encryption schemes pbeWithSHAAnd3-KeyTripleDES-CBC and pbewithSHAAnd40BitRC2-CBC, whose pkcs-12PbeParams iteration count is also read from the input. With an iteration count of 0 or less, the encryption key and IV are constants (0x01 and 0x02 repeated). This is reached from shrouded key bags and encrypted SafeContents in pkcs12FromAsn1(), and from forge.pki.decryptPrivateKeyInfo() (for example for ENCRYPTED PRIVATE KEY PEM input).

RFC 7292 defines MacData with iterations INTEGER DEFAULT 1; an iteration count only makes sense as 1 or more (RFC 8018 uses INTEGER (1..MAX) for the same concept). PBES2 (PBKDF2) encrypted content is not affected.

The fix rejects iteration counts less than 1 when verifying a PKCS#12 MAC in pkcs12FromAsn1() ("PKCS#12 MAC iteration count must be >= 1.") and at the entry of pki.pbe.generatePkcs12Key() ("PKCS#12 key derivation iteration count must be >= 1."), which covers the MAC, both PKCS#12 PBE schemes and direct forge.pkcs12.generateKey() callers.

Patches

Fixed in node-forge 1.4.1. Iteration counts less than 1 are now rejected when verifying a PKCS#12 MAC and in the PKCS#12 key derivation function.

Workarounds

Before calling forge.pkcs12.pkcs12FromAsn1(), reject inputs whose MacData.iterations is present and less than 1. For encrypted private keys using the PKCS#12 PBE schemes, there is no simple workaround other than checking the pkcs-12PbeParams iteration count before decryption, or upgrading.

Credits

Reported by Thai, Calif.io in collaboration with Claude and Anthropic Research.

Proof of concept

Take any PKCS#12/PFX file, replace its AuthenticatedSafe with attacker-chosen content (for example the attacker's certificate and an unencrypted private key), set MacData.iterations to 0 (DER 02 01 00), and compute the HMAC-SHA1 over the new content with the key 0x03 repeated 20 times. On node-forge <= 1.4.0, forge.pkcs12.pkcs12FromAsn1(asn1, password) reports successful MAC verification for whatever password the application supplies and returns the attacker's certificate and key. An empty or negative iteration count behaves the same way. Likewise, a shrouded key bag or ENCRYPTED PRIVATE KEY using pbeWithSHAAnd3-KeyTripleDES-CBC with iteration count 0 decrypts with any password.

The constant key is visible directly:

var salt = new forge.util.ByteBuffer('anysalt');
forge.pkcs12.generateKey('any password', salt, 3, 0, 20).toHex();
// <= 1.4.0: '0303030303030303030303030303030303030303'
// 1.4.1: throws "PKCS#12 key derivation iteration count must be >= 1."

The fix adds regression tests in tests/unit/pkcs12.js and the reporter's PoC in tests/pocs/.

Impact

Applications that import PKCS#12/PFX files or PKCS#8 encrypted private keys from untrusted sources with forge.pkcs12.pkcs12FromAsn1() or forge.pki.decryptPrivateKeyInfo(), and rely on the password-based MAC or on successful decryption to establish that the file was produced by someone who knows the password. The default API and call pattern are affected; no password, oracle or user interaction is needed, only one offline HMAC.

An attacker can supply a PKCS#12 file with certificates and private keys of their choice (for example, replaced certificates or an unencrypted private key) that node-forge reports as verified under the application's password. The attacker does not learn the password or the original private key. PBES2 (PBKDF2) encrypted content is not affected.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Forge: AES-GCM authentication bypass via unvalidated tagLength option

CVE-2026-119186 / GHSA-5cwg-6vw7-rj6p

More information

Details

Summary

The AES-GCM mode in forge.cipher does not validate the tagLength option passed to start(). With a tagLength of 0 or a value that converts to 0 (such as null, false, '' or '0'), the authentication tag is empty, and decipher.finish() returns true for any ciphertext and additional data when the supplied tag is empty or missing. Fixed in node-forge 1.4.1.

Details

modes.gcm.prototype.start() in lib/cipherModes.js stored the tagLength option without validation, and lib/cipher.js documented it as accepting "0-128". After processing the data, afterFinish() truncates the 16-byte tag with truncate(16 % (tagLength / 8)). For a tagLength that converts to 0, this is 16 % 0 = NaN, and truncating by NaN empties the tag. On decryption, the start-time check that the supplied tag has tagLength / 8 bytes passes for an empty tag, and the final tag comparison compares two empty strings, so decipher.finish() returns true for any ciphertext and additional data.

NIST SP 800-38D section 5.2.1.2 allows only tag lengths of 128, 120, 112, 104 and 96 bits, plus 64 and 32 bits for certain applications.

The fix validates tagLength in start(), for both encryption and decryption. A missing, null or undefined tagLength uses the default of 128 bits. Any other value is converted with Number() and must be one of 128, 120, 112, 104, 96, 64 or 32; otherwise start() throws "Invalid tag length.". Numeric strings such as '128' are still accepted. With a validated tagLength, an empty tag no longer matches the expected tag length and is rejected.

Patches

Fixed in node-forge 1.4.1. cipher.start() now throws an error unless tagLength is one of the tag lengths allowed by NIST SP 800-38D (128, 120, 112, 104, 96, 64 or 32 bits); a null or undefined tagLength means the default of 128 bits.

Workarounds

Do not derive tagLength from received data. Pass a fixed tagLength (or omit it to use the default of 128 bits), and check that the received tag has the expected length before calling start(), for example if(tag.length !== 16) { throw new Error('Invalid tag.'); } before decipher.start({iv: iv, tag: tag});.

Proof of concept

Start an AES-GCM decipher with tagLength: 0 and an empty tag, as an application does when it derives the tag length from a received empty tag, and feed it arbitrary bytes that were never encrypted under the key:

var decipher = forge.cipher.createDecipher('AES-GCM', key);
decipher.start({iv: iv, tag: '', tagLength: 0});
decipher.update(forge.util.createBuffer('any attacker-chosen bytes'));
decipher.finish();
// <= 1.4.0: true
// 1.4.1: start() throws "Invalid tag length."

tagLength values null, false and '0' (with an empty tag), and tagLength: 0 with no tag option, give the same result on <= 1.4.0. The fix adds regression tests in tests/unit/aes.js.

Impact

Applications that decrypt AES-GCM data with forge.cipher.createDecipher() ('AES-GCM') and pass a zero-like tagLength are affected. Typical cases are a tag length derived from the received tag, or an unset configuration value:

var decipher = forge.cipher.createDecipher('AES-GCM', key);
decipher.start({iv: iv, tag: receivedTag, tagLength: receivedTag.length * 8});
decipher.update(forge.util.createBuffer(receivedCiphertext));
if(decipher.finish()) { /* attacker-supplied data accepted */ }

An attacker who can send a ciphertext with an empty tag to such an application can make it accept attacker-chosen ciphertext as authentic. Because GCM encryption is a stream cipher, an attacker who knows (part of) a captured message's plaintext can also change that plaintext to chosen values. The default (no tagLength option, 128 bits) and any fixed valid tagLength are not affected.

Severity

  • CVSS Score: 5.9 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Forge: verifyCertificateChain() removes trusted CA certificates with the same subject from the CA store

CVE-2026-118780 / GHSA-4487-j8r4-4g2f

More information

Details

Summary

When a CA store created by pki.createCaStore() holds two or more certificates with the same subject name, pki.verifyCertificateChain() permanently removes those certificates from the store while trying each one as the issuer. A peer can send any certificate that names that subject as its issuer to make all later verifications against those CA certificates fail. Fixed in node-forge 1.4.1.

Details

pki.verifyCertificateChain() (lib/x509.js) asks the CA store for the issuer of each certificate with caStore.getIssuer(). When the store holds several certificates with the same subject, caStore.getIssuer() returns the store's own internal array of those certificates (caStore.certs[hash], built by addCertificate()). verifyCertificateChain() used that array directly as its list of candidate parents and called parents.shift() on it while trying each candidate. Every candidate it tried, including the one that finally verified the certificate, was removed from the store.

Once the array is empty, getIssuer() returns an empty list, no candidate is tried, and verification fails with "Certificate signature is invalid." hasCertificate() and listAllCertificates() no longer see the removed certificates. The store stays in this state for as long as the object is used.

The attacker's certificate only needs an issuer name equal to the shared subject name, which is public. Its signature and key can be arbitrary: every candidate is tried and shifted out because none verifies, and errors from parent.verify() are caught by the loop. Verification of the attacker's chain fails as expected. Verifying valid chains has a similar effect: the CA certificate that verified the chain and any tried before it are removed.

No chain is accepted that should be rejected. Removing candidates can only turn a success into a failure.

RFC 5280, Section 6.1.1(d) treats trust anchor information as an input to path validation. Validation should not consume it.

The fix copies the array returned by getIssuer() before iterating over it, so the CA store is no longer modified. getIssuer() itself is unchanged.

Patches

Fixed in node-forge 1.4.1. pki.verifyCertificateChain() now works on a copy of the candidate issuer list instead of shifting entries out of the CA store.

Workarounds
  • Pass a fresh CA store to each pki.verifyCertificateChain() call (for example pki.createCaStore(trustedCerts)), or pass caStore to forge.tls.createConnection() as an array of certificates instead of a store object, so that forge creates a new store per connection.

  • Or wrap the store so that getIssuer() returns a copy:

    var getIssuer = caStore.getIssuer;
    caStore.getIssuer = function(cert) {
      var r = getIssuer(cert);
      return Array.isArray(r) ? r.slice() : r;
    };
    
Proof of concept

Two trusted self-signed CA certificates, rootA and rootB, share the subject CN=Root. bogus is any certificate whose issuer is CN=Root, signed with an unrelated key. leafA is a valid certificate issued by rootA.

var caStore = forge.pki.createCaStore([rootA, rootB]);
caStore.listAllCertificates().length;  // 2
try { forge.pki.verifyCertificateChain(caStore, [bogus]); } catch(e) {}
// rejected as expected ("Certificate signature is invalid.")
caStore.listAllCertificates().length;  // 0: both CA certificates are gone
forge.pki.verifyCertificateChain(caStore, [leafA]);
// throws "Certificate signature is invalid." (valid chain now rejected)

Without the attacker certificate, verifying leafA twice against a fresh store gives the same result: the first call succeeds and removes rootA, the second fails. With the fix, the store keeps both certificates and every valid chain verifies. The regression tests are in tests/unit/x509.js ("CA store" section).

Impact

Applications that keep a long-lived CA store holding several trusted certificates with the same subject name, and that verify certificate chains received from other parties with pki.verifyCertificateChain(). This includes forge.tls clients that pass the same caStore object to several connections. Duplicate subject names occur with re-keyed or renewed CAs that kept their name, or a CA next to a cross-signed version of itself, mostly in private PKIs.

A remote, unauthenticated peer (for example a malicious TLS server or a party submitting a signed document) can send a certificate naming the shared subject as its issuer. This removes every certificate with that subject from the store, so later verifications of valid chains issued by those CA certificates fail until the store is recreated. This is a persistent denial of service for certificate verification against those CA certificates. Verification against other trusted CAs keeps working, no invalid chain is accepted, and stores without duplicate subject names are not affected.

Severity

  • CVSS Score: 3.7 / 10 (Low)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Forge: Denial of Service via infinite loop in BigInteger.modInverse() with negative input (incomplete fix of CVE-2026-33891)

CVE-2026-118781 / GHSA-h6cr-wjv9-3cpp

More information

Details

Summary

BigInteger.prototype.modInverse() in node-forge's bundled jsbn library enters an infinite loop when the value or the modulus is negative, for example when a negative public exponent is passed to forge.pki.rsa.generateKeyPair(). This is an incomplete fix of CVE-2026-33891 (GHSA-5m6q-g25r-mvwx), which handled only a zero value. Fixed in node-forge 1.4.1.

Details

bnModInverse() (lib/jsbn.js, exposed as forge.jsbn.BigInteger.prototype.modInverse) implements the binary extended Euclidean algorithm (Handbook of Applied Cryptography, Algorithm 14.61), which is defined for positive integers. With a negative operand the loop cannot terminate: rShiftTo is an arithmetic shift, so a negative value never moves towards zero, and the loop's exit condition (u reaching zero) is never met. The calling thread spins at 100% CPU indefinitely; in Node.js this blocks the event loop. Memory does not grow.

node-forge 1.4.0 fixed CVE-2026-33891 (GHSA-5m6q-g25r-mvwx) by returning zero early when this.signum() == 0, although its changelog describes the check as "<= 0". Negative values were not covered, and a negative modulus was never handled. Every negative input that did terminate already returned zero.

Within node-forge, RSA key generation is the only path to a negative operand: the caller's public exponent e becomes BigInteger.fromInt(e), and state.e.modInverse(state.phi) is called with it. gcd() works on absolute values, so the coprimality checks pass and the loop is reached. Any exponent other than 3 or 65537 uses the pure JavaScript implementation, even when forge.options.usePureJavaScript is not set. RFC 8017, Section 3.1 requires the RSA public exponent to be at least 3.

Values that node-forge parses (DER, PEM, JWK, PKCS#12, X.509, TLS) are always read as unsigned integers and cannot produce a negative operand.

The fix makes modInverse() return zero when the value or the modulus is zero or negative, as it already does when no inverse exists. Results for positive inputs are unchanged.

This issue was reported by wzc.

Patches

Fixed in node-forge 1.4.1. modInverse() now returns zero when the value or the modulus is zero or negative.

Workarounds
  • Pass only fixed, valid RSA public exponents (e.g. 65537) to key generation, or validate caller-supplied exponents first: an odd integer >= 3.
  • Do not call BigInteger#modInverse() with negative operands. Reduce the value modulo a positive modulus first (x.mod(m).modInverse(m)).
Proof of concept

A negative public exponent passed to RSA key generation never returns (also with the callback form and with createKeyPairGenerationState() / stepKeyPairGenerationState()):

var forge = require('node-forge');
forge.pki.rsa.generateKeyPair({bits: 256, e: -1});  // hangs at 100% CPU

Calling modInverse() directly with a negative value or modulus hangs the same way:

var BigInteger = forge.jsbn.BigInteger;
new BigInteger('-1').modInverse(new BigInteger('7'));  // hangs
new BigInteger('3').modInverse(new BigInteger('-7'));  // hangs

With the fix, these calls return zero. The regression tests are in tests/unit/jsbn.js.

Impact

Applications are affected only if they:

  • pass an untrusted value as the RSA public exponent e to forge.pki.rsa.generateKeyPair(), forge.pki.rsa.createKeyPairGenerationState() or forge.pki.rsa.stepKeyPairGenerationState(), or
  • call forge.jsbn.BigInteger#modInverse() directly with negative values derived from untrusted input.

An attacker who can supply such a value makes the call hang indefinitely at 100% CPU, which blocks the Node.js event loop (denial of service). Keys and other data parsed by node-forge (DER, PEM, JWK, PKCS#12, X.509, TLS) cannot trigger this issue, and the documented usage of key generation passes a constant exponent (65537). There is no effect on confidentiality or integrity.

Severity

  • CVSS Score: 2.9 / 10 (Low)
  • Vector String: CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Forge: Regular expression denial of service in PEM decoding (pem.decode)

CVE-2026-118779 / GHSA-7wch-p9px-956x

More information

Details

Summary

pem.decode(), which every node-forge API that parses PEM input uses, split messages with a regular expression that takes polynomial time on input without a matching END line, and parsed encapsulated headers with regular expressions that take quadratic time on long header lines. A remote attacker can block the Node.js event loop for seconds to minutes with a crafted input of a few tens of kilobytes. Fixed in node-forge 1.4.1.

Details

pem.decode() in lib/pem.js found PEM messages with this regular expression:

/\s*-----BEGIN ([A-Z0-9- ]+)-----\r?\n?([\x21-\x7e\s]+?(?:\r?\n\r?\n))?([:A-Za-z0-9+\/=\s]+?)-----END \1-----/g

The optional headers group and the body group are adjacent, lazy, and share characters (whitespace and the base64 alphabet), and the match must end with an END line whose label equals the BEGIN label (\1). When no such END line follows, the engine tries every split of the text between the two groups, for every label split and at every BEGIN line in the input. The work grows polynomially with the input length (roughly cubic for repeated BEGIN/END lines with mismatched labels, quadratic for a BEGIN line followed by many newlines).

The encapsulated header parsing in the same function had two smaller quadratic cases: trimming each header line with /\s+$/, and an unanchored header regex ([\x21-\x7e]+):... that retries at every position of a long line without a colon. These are reachable when a message does match.

All PEM entry points call pem.decode(), including pki.certificateFromPem(), pki.privateKeyFromPem(), pki.publicKeyFromPem(), pki.certificationRequestFromPem(), pki.encryptedPrivateKeyFromPem(), pki.decryptRsaPrivateKey() and pkcs7.messageFromPem().

The fix replaces the message regular expression with a scanner that indexes the END lines once and matches each BEGIN line in linear time, finding the same messages as before. The one difference: a label containing ----- (-----BEGIN A-----B-----) was matched as A-----B if possible; now only the shortest label, A, is used. RFC 7468 labels can't contain --. Header parsing now trims lines with a loop and only starts a header match at the beginning of a run of printable characters, which gives the same result.

Patches

Fixed in node-forge 1.4.1. PEM messages and encapsulated headers are now parsed in linear time.

Workarounds

Rejecting untrusted PEM input larger than a few kilobytes before passing it to node-forge limits the delay but doesn't remove it: on 1.4.0, about 7 KB of crafted input blocked for 0.4 s, and 29 KB for 24 s.

Credits

Reported by Ahmed Mokhtari (@​AhmedMokhtari), @​mwlik and @​imenyoo2, and independently by @​offset, @​bibu123456 (coordinated by @​Kayiz-PT), and @​Mucahic.

Proof of concept
var forge = require('node-forge');
var pem = ' -----BEGIN I-----G-----END B'.repeat(1000) + '\x00'; // 29 KB
try {
  forge.pki.certificateFromPem(pem);
} catch(e) {}

On 1.4.0 (Node.js 24) this call blocks for about 24 seconds before throwing "Invalid PEM formatted message."; 14 KB takes about 3 seconds. A BEGIN line followed by 32 KB of newlines and no END line takes about 1.5 seconds, and the time grows quadratically from there. On 1.4.1 each of these inputs is rejected in a few milliseconds, and 1 MB inputs of any of these shapes take under 100 ms.

The PoC is in tests/pocs/GHSA-7wch-p9px-956x.js. Regression tests are in tests/unit/pem.js ("GHSA-7wch-p9px-956x ReDoS checks").

Impact

Any application that passes untrusted PEM text to node-forge is affected: for example, services that accept uploaded certificates, keys or certificate signing requests, or that parse certificates from configuration, SAML metadata or other user-supplied documents. A remote, unauthenticated attacker can send a small crafted input that blocks the Node.js event loop for seconds to minutes per request, so the process stops serving other requests. Repeated requests keep it unavailable. There is no confidentiality or integrity impact.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Forge: TLS client crash when the server sends no certificate

CVE-2026-118778 / GHSA-525v-9gp9-w22h

More information

Details

Summary

The forge TLS client's check for a missing server certificate read the wrong property and never ran. A malicious TLS server, or an on-path attacker who removes the server's Certificate message, can make connection.process() throw a TypeError, which crashes applications that use the forge TLS client. Fixed in node-forge 1.4.1.

Details

After ServerHelloDone, tls.handleServerHelloDone() in lib/tls.js is meant to reject a server that sent no certificate with a fatal insufficient_security alert, after consulting the application's verify callback. The check tested c.serverCertificate, which is never set (the certificate is stored as c.session.serverCertificate), so it never ran. The client state machine allows ServerHelloDone directly after ServerHello, so the handshake continued to tls.createClientKeyExchange(), which reads c.session.serverCertificate.publicKey and throws a TypeError on the null certificate. The exception propagates out of connection.process() without calling the verify or error callbacks.

There is no authentication bypass: without a server certificate the client cannot build its RSA key exchange, so the handshake cannot complete.

The fix reads c.session.serverCertificate and always fails the handshake in that case, still calling verify so the application can customize the alert. Failing even when verify returns true is needed because the key exchange requires the certificate.

Patches

Fixed in node-forge 1.4.1. The TLS client fails the handshake with an insufficient_security alert when the server sends no certificate.

Workarounds

Catch exceptions thrown by connection.process() in the code that feeds it network data, and close the connection when one occurs.

Credits

Reported by @​arpitjain099.

Proof of concept

Connect a forge TLS client (forge.tls.createConnection({server: false, ...})) to a server whose first flight contains ServerHello and ServerHelloDone but no Certificate message. For example, run a forge TLS server and remove the Certificate handshake message (type 11) from its first flight before passing it to the client's process().

On 1.4.0 the client's process() throws "TypeError: Cannot read properties of null (reading 'publicKey')", and the verify and error callbacks are not called. On 1.4.1 the client calls verify with insufficient_security and no certificates, calls error with "No server certificate provided. Not enough security.", sends a fatal alert and closes the connection.

Regression tests are in tests/unit/tls.js ("missing server certificate").

Impact

Applications that use the forge TLS client (forge.tls.createConnection with server: false) are affected. A malicious server the application connects to, or an on-path attacker who can modify the unauthenticated handshake, can make the client throw from connection.process(). Since process() is normally called from a socket data handler, the uncaught exception terminates a Node.js process. There is no confidentiality or integrity impact. Applications using Node.js's own TLS implementation are not affected.

Severity

  • CVSS Score: 5.9 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Forge: Prototype Pollution in forge.http cookie handling via a malicious Set-Cookie

CVE-2026-118777 / GHSA-6wvx-3r32-xmcg

More information

Details

Summary

The forge.http client stored cookies from Set-Cookie response headers in a plain object keyed by cookie name and path. A malicious server could use the cookie name __proto__ to add properties to Object.prototype, use inherited names such as constructor to add properties to Object, and send an empty cookie value to delete Object.prototype properties. Fixed in node-forge 1.4.1.

Details

client.setCookie() in lib/http.js stored a cookie with client.cookies[cookie.name][cookie.path] = cookie, creating client.cookies[cookie.name] only when !(cookie.name in client.cookies). Both values come from the server's Set-Cookie header via response.getCookies(). Because in also sees inherited properties:

  • a cookie named __proto__ skipped the initialization, client.cookies['__proto__'] returned Object.prototype, and the cookie path was written as a new Object.prototype property, visible on every object;
  • a cookie named constructor wrote onto the global Object function the same way;
  • a cookie with an empty value calls client.removeCookie(name, path), which used the same in checks and then delete client.cookies[name][path], so Set-Cookie: __proto__=; path=toString deleted Object.prototype.toString. This path skipped the cookie's secure-flag and domain checks, so it also worked for http:// clients.

forge.http is not loaded by require('node-forge'). It is included in the forge.all browser bundle and is used with a socket pool, for example by forge.xhr.

The fix uses own-property checks for the cookie store and ignores cookies named __proto__ or with the path __proto__.

Patches

Fixed in node-forge 1.4.1. Cookies from a server can no longer add or remove properties of Object.prototype or Object.

Workarounds

Don't use forge.http or forge.xhr with servers that aren't trusted, or filter Set-Cookie headers named __proto__, constructor or other Object.prototype property names before they reach the client.

Credits

Reported by @​arunmozhivarman-dtx and @​Dremig.

Proof of concept

A server response for a forge.http client (http.createClient({url: 'https://...', socketPool: ...})) containing:

Set-Cookie: __proto__=evil; path=polluted

On 1.4.0, after the client processes the response, ({}).polluted is the cookie object. With Set-Cookie: __proto__=; path=toLocaleString, Object.prototype.toLocaleString is deleted (for http:// and https:// clients). On 1.4.1 these cookies are ignored and Object.prototype is unchanged.

Regression tests are in tests/security/GHSA-6wvx-3r32-xmcg.js.

Impact

Applications that use the forge.http client (directly, or through forge.xhr in the forge.all browser bundle) against a server that can be malicious are affected. The server can add properties to Object.prototype and Object for the whole JavaScript realm, which can change application logic that reads properties from plain objects, and can delete built-in Object.prototype methods, breaking the page or process. Applications that don't use forge.http are not affected.

Severity

  • CVSS Score: 4.8 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Forge: ASN.1 OID first arc truncation in asn1.oidToDer

CVE-2026-118775 / GHSA-4642-5mcj-2rqw

More information

Details

Summary

asn1.oidToDer() did not range-check the first two arcs of an OID string, so an OID such as 0.42.840.113549.1.1.1 or 4294977127.18.840.113549.1.1.1 was encoded as the DER of a different OID (here rsaEncryption, 1.2.840.113549.1.1.1). This is an incomplete fix of CVE-2025-66030. Fixed in node-forge 1.4.1.

Details

X.690 section 8.19 encodes the first two arcs of an OID as one subidentifier, 40 * arc1 + arc2, where the first arc is 0, 1 or 2 and the second arc is at most 39 when the first is 0 or 1. asn1.oidToDer() in lib/asn1.js computed that value with no range check and wrote it with putByte(), which keeps only the low 16 bits. Out-of-range first arcs therefore produced the encoding of a different OID: 0.42.840.113549.1.1.1 gives 40 * 0 + 42 = 42 and 4294977127.18.840.113549.1.1.1 wraps to 42, both the first byte of rsaEncryption. The fix for CVE-2025-66030 (GHSA-65ch-62r8-g69g, node-forge 1.3.2) added a 32-bit check for the later arcs only.

A related encoding bug made valid OIDs with a first subidentifier above 127 (2.48 and higher, e.g. 2.999) encode and decode incorrectly, because the first subidentifier was always written and read as a single byte.

The fix rejects out-of-range first arcs and encodes and decodes the first subidentifier in base 128 like the others (the latter from public PR #​1148).

Patches

Fixed in node-forge 1.4.1. asn1.oidToDer() throws for OIDs with out-of-range first arcs, and the first subidentifier is encoded and decoded correctly.

Workarounds

Validate OID strings from untrusted input before passing them to node-forge: the first arc must be 0, 1 or 2, and the se

[!IMPORTANT]
✂ PR body was truncated to here.

@renovate renovate Bot added the dependencies Pull requests that update a dependency file label Nov 2, 2026
@renovate
renovate Bot requested a review from a team as a code owner November 2, 2026 11:32
@renovate renovate Bot added the dependencies Pull requests that update a dependency file label Nov 2, 2026
@cit-pr-commenter-54b7da

Copy link
Copy Markdown

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum Salesforce 190.87 KiB 190.87 KiB 0 B 0.00% ✅
Rum 190.84 KiB 190.84 KiB 0 B 0.00% ✅
Rum Salesforce Profiler 8.35 KiB 8.35 KiB 0 B 0.00% ✅
Rum Profiler 8.35 KiB 8.35 KiB 0 B 0.00% ✅
Rum Salesforce Recorder 32.04 KiB 32.04 KiB 0 B 0.00% ✅
Rum Recorder 32.04 KiB 32.04 KiB 0 B 0.00% ✅
Logs 59.78 KiB 59.78 KiB 0 B 0.00% ✅
Rum Slim 146.99 KiB 146.99 KiB 0 B 0.00% ✅
Worker 24.23 KiB 24.23 KiB 0 B 0.00% ✅
Rum Shopify 221.29 KiB 221.29 KiB 0 B 0.00% ✅
Rum Shopify Profiler 8.35 KiB 8.35 KiB 0 B 0.00% ✅
Rum Shopify Recorder 4.85 KiB 4.85 KiB 0 B 0.00% ✅

@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Pipelines  Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
• Patch Coverage: 100.00%
• Overall Coverage: 77.30% (+0.00%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 5da52c6 | Docs | View more details | Give us feedback!

@datadog-datadog-prod-us1 datadog-datadog-prod-us1 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bits Code Review: PASS

More details

The dependency pin and lockfile agree on node-forge 1.4.1. The existing certificate-generation path passed a targeted compatibility check covering Node TLS context creation and certificate verification.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Bits Code Review · Commit 5da52c6

@thomas-lebeau
thomas-lebeau merged commit e2c8f50 into main Nov 2, 2026
31 checks passed
@thomas-lebeau
thomas-lebeau deleted the renovate/npm-node-forge-vulnerability branch November 2, 2026 12:20
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant