Repository navigation
👷 Update dependency node-forge to v1.4.1 [SECURITY] - #5221
Merged
Merged
Conversation
Bundles Sizes Evolution
|
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: 5da52c6 | Docs | View more details | Give us feedback! |
There was a problem hiding this comment.
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.
🤖 Bits Code Review · Commit 5da52c6
thomas-lebeau
approved these changes
Nov 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
This PR body was truncated due to platform limits.
This PR contains the following updates:
1.4.0→1.4.1Warning
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.10and2.5.29.13asbasicConstraints, andforge.pki.verifyCertificateChain()used the first extension with that name. A certificate with a forged deprecated-OID extension (cATRUE) before its genuinebasicConstraintsextension (cAFALSE) is accepted as a CA certificate, and the same technique can hide apathLenConstraint. Fixed in node-forge 1.4.1.Details
lib/oids.jsmaps the deprecated certificate extension OIDs2.5.29.10and2.5.29.13to the namebasicConstraints, the same name as the RFC 5280 extension2.5.29.19.certificateExtensionFromAsn1()inlib/x509.jsdecodes extensions by name, so these extensions are decoded into the samecAandpathLenConstraintfields.cert.getExtension()returns the first extension with a matching name, andpki.verifyCertificateChain()looked upbasicConstraintsby name.As a result, a certificate that contains a
2.5.29.10(or2.5.29.13) extension withcATRUE before its genuinebasicConstraintsextension withcAFALSE is accepted as a CA certificate. The same technique can hide a CA certificate'spathLenConstraint. A deprecated-OID extension on its own also satisfies the check added for GHSA-2328-f5f3-gj25 (CVE-2026-33896) thatbasicConstraintsis present. RFC 5280 section 4.2.1.9 defines basicConstraints only asid-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 thecAand thepathLenConstraintchecks. Certificate parsing, the OID table andcert.getExtension('basicConstraints')are unchanged.Patches
Fixed in node-forge 1.4.1. Chain verification now uses only the
basicConstraintsextension with OID2.5.29.19.Workarounds
None in node-forge itself. Applications that pass an
options.verifycallback topki.verifyCertificateChain()(orverifytoforge.tls.createConnection()) can reject chains in which a non-leaf certificate has no2.5.29.19extension withcATRUE, for example withcert.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(or2.5.29.13) extension with the value of a basicConstraintscATRUE (DER30 03 01 01 FF), then the CA's own2.5.29.19basicConstraints withcAFALSE, and nokeyUsage. For example,openssl x509 -req -copy_extensions copywith an extensions file that setsbasicConstraints = CA:FALSEcopies 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, andeeCert.getExtension('basicConstraints')returns the2.5.29.10extension withcAtrue. 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.jsand the reporter's PoC astests/pocs/ghsa-5x38-2vff-x55p.js.Impact
Applications that verify certificate chains with
forge.pki.verifyCertificateChain(), including TLS peer certificate verification inforge.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(orcopyall) with an extensions file that setsbasicConstraints = CA:FALSEand nokeyUsageproduces such a certificate. If the issuing CA adds akeyUsageextension withoutkeyCertSign, 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:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:NReferences
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, andprocess()throwsRangeError: 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 fromctTable. Every handler (tls.handleHeartbeat,tls.handleApplicationData,tls.handleHandshakefor fragmented messages,tls.handleHelloRequestand the other handshake handlers) ends by callingc.process()again to continue with the next buffered record, or the next message in the same record. Each record therefore addsprocess→_update→ handler to the call stack. In Node.js 24, about 2,300 eight-byte records (under 20 KB) in one chunk are enough forprocess()to throwRangeError: Maximum call stack size exceeded.ctTablemaps heartbeat records totls.handleHeartbeatin 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
errorcallback, so applications do not normally wrapprocess()intry/catch. Whenprocess()is called from a Node.js socketdatahandler, 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(), andc.process()runs it in a loop. A handler's call toc.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()intry/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()throwsRangeError: Maximum call stack size exceededafter about 2,300 records, before any handshake. The same happens for a client connection. In a Node.js server that callsc.process(data.toString('binary'))fromsocket.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 byforge.tls.wrapSocket()and theforge.http/forge.xhrTLS connections, which callprocess()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:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
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 uncaughtTypeErrorfromprocess()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." throughc.error()with a fatalunexpected_messagealert. That fatal error closes and resets the connection, which setsc.recordtonull._readRecord()then continues withc.record.ready = trueand throwsTypeError: Cannot set properties of null (setting 'ready')out ofprocess(). 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_messageis the intended behavior; the bug is the crash after the error.node-forge reports TLS protocol errors through the
errorcallback, so applications do not normally wrapprocess()intry/catch. Whenprocess()is called from a Node.js socketdatahandler, 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 theerrorcallback is called as before, andprocess()no longer throws.Patches
Fixed in node-forge 1.4.1. The connection still fails with a fatal
unexpected_messagealert and theerrorcallback, butprocess()no longer throws.Workarounds
Wrap every call to
connection.process()intry/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
errorcallback receives "Invalid fragmented record.", thenprocess()throwsTypeError: 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 callsc.process(data.toString('binary'))fromsocket.on('data', ...), a client sending these 18 bytes terminates the server process. On 1.4.1 the connection fails with a fatalunexpected_messagealert andprocess()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 byforge.tls.wrapSocket()and theforge.http/forge.xhrTLS 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:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
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()inlib/pkcs12.jsreads the MAC iteration count (MacData.iterations) from the input and passes it to the PKCS#12 key derivation functionpki.pbe.generatePkcs12Key()inlib/pbe.js(also exposed asforge.pkcs12.generateKey()) without checking that it is at least 1. The key derivation function (RFC 7292 Appendix B.2) runs its hash loopitertimes. 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 example0x03repeated 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 schemespbeWithSHAAnd3-KeyTripleDES-CBCandpbewithSHAAnd40BitRC2-CBC, whosepkcs-12PbeParamsiteration count is also read from the input. With an iteration count of 0 or less, the encryption key and IV are constants (0x01and0x02repeated). This is reached from shrouded key bags and encrypted SafeContents inpkcs12FromAsn1(), and fromforge.pki.decryptPrivateKeyInfo()(for example forENCRYPTED PRIVATE KEYPEM input).RFC 7292 defines
MacDatawithiterations INTEGER DEFAULT 1; an iteration count only makes sense as 1 or more (RFC 8018 usesINTEGER (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 ofpki.pbe.generatePkcs12Key()("PKCS#12 key derivation iteration count must be >= 1."), which covers the MAC, both PKCS#12 PBE schemes and directforge.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 whoseMacData.iterationsis present and less than 1. For encrypted private keys using the PKCS#12 PBE schemes, there is no simple workaround other than checking thepkcs-12PbeParamsiteration 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.iterationsto 0 (DER02 01 00), and compute the HMAC-SHA1 over the new content with the key0x03repeated 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 orENCRYPTED PRIVATE KEYusingpbeWithSHAAnd3-KeyTripleDES-CBCwith iteration count 0 decrypts with any password.The constant key is visible directly:
The fix adds regression tests in
tests/unit/pkcs12.jsand the reporter's PoC intests/pocs/.Impact
Applications that import PKCS#12/PFX files or PKCS#8 encrypted private keys from untrusted sources with
forge.pkcs12.pkcs12FromAsn1()orforge.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:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:NReferences
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()inlib/cipherModes.jsstored thetagLengthoption without validation, andlib/cipher.jsdocumented it as accepting "0-128". After processing the data,afterFinish()truncates the 16-byte tag withtruncate(16 % (tagLength / 8)). For atagLengththat converts to 0, this is16 % 0 = NaN, and truncating byNaNempties the tag. On decryption, the start-time check that the suppliedtaghastagLength / 8bytes passes for an empty tag, and the final tag comparison compares two empty strings, sodecipher.finish()returnstruefor 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
tagLengthinstart(), for both encryption and decryption. A missing,nullorundefinedtagLengthuses the default of 128 bits. Any other value is converted withNumber()and must be one of 128, 120, 112, 104, 96, 64 or 32; otherwisestart()throws "Invalid tag length.". Numeric strings such as'128'are still accepted. With a validatedtagLength, 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 unlesstagLengthis one of the tag lengths allowed by NIST SP 800-38D (128, 120, 112, 104, 96, 64 or 32 bits); anullorundefinedtagLengthmeans the default of 128 bits.Workarounds
Do not derive
tagLengthfrom received data. Pass a fixedtagLength(or omit it to use the default of 128 bits), and check that the received tag has the expected length before callingstart(), for exampleif(tag.length !== 16) { throw new Error('Invalid tag.'); }beforedecipher.start({iv: iv, tag: tag});.Proof of concept
Start an AES-GCM decipher with
tagLength: 0and 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:tagLengthvaluesnull,falseand'0'(with an empty tag), andtagLength: 0with notagoption, give the same result on <= 1.4.0. The fix adds regression tests intests/unit/aes.js.Impact
Applications that decrypt AES-GCM data with
forge.cipher.createDecipher()('AES-GCM') and pass a zero-liketagLengthare affected. Typical cases are a tag length derived from the received tag, or an unset configuration value: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
tagLengthoption, 128 bits) and any fixed validtagLengthare not affected.Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:NReferences
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 withcaStore.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 byaddCertificate()).verifyCertificateChain()used that array directly as its list of candidate parents and calledparents.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()andlistAllCertificates()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 examplepki.createCaStore(trustedCerts)), or passcaStoretoforge.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:Proof of concept
Two trusted self-signed CA certificates,
rootAandrootB, share the subjectCN=Root.bogusis any certificate whose issuer isCN=Root, signed with an unrelated key.leafAis a valid certificate issued byrootA.Without the attacker certificate, verifying
leafAtwice against a fresh store gives the same result: the first call succeeds and removesrootA, the second fails. With the fix, the store keeps both certificates and every valid chain verifies. The regression tests are intests/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 includesforge.tlsclients that pass the samecaStoreobject 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:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:LReferences
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 toforge.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 asforge.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:rShiftTois an arithmetic shift, so a negative value never moves towards zero, and the loop's exit condition (ureaching 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
ebecomesBigInteger.fromInt(e), andstate.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 whenforge.options.usePureJavaScriptis 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
>= 3.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()):Calling
modInverse()directly with a negative value or modulus hangs the same way:With the fix, these calls return zero. The regression tests are in
tests/unit/jsbn.js.Impact
Applications are affected only if they:
etoforge.pki.rsa.generateKeyPair(),forge.pki.rsa.createKeyPairGenerationState()orforge.pki.rsa.stepKeyPairGenerationState(), orforge.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:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:LReferences
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()inlib/pem.jsfound PEM messages with this regular expression: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(), includingpki.certificateFromPem(),pki.privateKeyFromPem(),pki.publicKeyFromPem(),pki.certificationRequestFromPem(),pki.encryptedPrivateKeyFromPem(),pki.decryptRsaPrivateKey()andpkcs7.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 asA-----Bif 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
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:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
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 aTypeError, which crashes applications that use the forge TLS client. Fixed in node-forge 1.4.1.Details
After ServerHelloDone,
tls.handleServerHelloDone()inlib/tls.jsis meant to reject a server that sent no certificate with a fatalinsufficient_securityalert, after consulting the application'sverifycallback. The check testedc.serverCertificate, which is never set (the certificate is stored asc.session.serverCertificate), so it never ran. The client state machine allows ServerHelloDone directly after ServerHello, so the handshake continued totls.createClientKeyExchange(), which readsc.session.serverCertificate.publicKeyand throws aTypeErroron the null certificate. The exception propagates out ofconnection.process()without calling theverifyorerrorcallbacks.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.serverCertificateand always fails the handshake in that case, still callingverifyso the application can customize the alert. Failing even whenverifyreturnstrueis 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_securityalert 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'sprocess().On 1.4.0 the client's
process()throws "TypeError: Cannot read properties of null (reading 'publicKey')", and theverifyanderrorcallbacks are not called. On 1.4.1 the client callsverifywithinsufficient_securityand no certificates, callserrorwith "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.createConnectionwithserver: 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 fromconnection.process(). Sinceprocess()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:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
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.httpclient stored cookies fromSet-Cookieresponse headers in a plain object keyed by cookie name and path. A malicious server could use the cookie name__proto__to add properties toObject.prototype, use inherited names such asconstructorto add properties toObject, and send an empty cookie value to deleteObject.prototypeproperties. Fixed in node-forge 1.4.1.Details
client.setCookie()inlib/http.jsstored a cookie withclient.cookies[cookie.name][cookie.path] = cookie, creatingclient.cookies[cookie.name]only when!(cookie.name in client.cookies). Both values come from the server'sSet-Cookieheader viaresponse.getCookies(). Becauseinalso sees inherited properties:__proto__skipped the initialization,client.cookies['__proto__']returnedObject.prototype, and the cookie path was written as a newObject.prototypeproperty, visible on every object;constructorwrote onto the globalObjectfunction the same way;client.removeCookie(name, path), which used the sameinchecks and thendelete client.cookies[name][path], soSet-Cookie: __proto__=; path=toStringdeletedObject.prototype.toString. This path skipped the cookie's secure-flag and domain checks, so it also worked forhttp://clients.forge.httpis not loaded byrequire('node-forge'). It is included in theforge.allbrowser bundle and is used with a socket pool, for example byforge.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.prototypeorObject.Workarounds
Don't use
forge.httporforge.xhrwith servers that aren't trusted, or filterSet-Cookieheaders named__proto__,constructoror otherObject.prototypeproperty names before they reach the client.Credits
Reported by @arunmozhivarman-dtx and @Dremig.
Proof of concept
A server response for a
forge.httpclient (http.createClient({url: 'https://...', socketPool: ...})) containing:On 1.4.0, after the client processes the response,
({}).pollutedis the cookie object. WithSet-Cookie: __proto__=; path=toLocaleString,Object.prototype.toLocaleStringis deleted (forhttp://andhttps://clients). On 1.4.1 these cookies are ignored andObject.prototypeis unchanged.Regression tests are in tests/security/GHSA-6wvx-3r32-xmcg.js.
Impact
Applications that use the
forge.httpclient (directly, or throughforge.xhrin theforge.allbrowser bundle) against a server that can be malicious are affected. The server can add properties toObject.prototypeandObjectfor the whole JavaScript realm, which can change application logic that reads properties from plain objects, and can delete built-inObject.prototypemethods, breaking the page or process. Applications that don't useforge.httpare not affected.Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:LReferences
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 as0.42.840.113549.1.1.1or4294977127.18.840.113549.1.1.1was 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()inlib/asn1.jscomputed that value with no range check and wrote it withputByte(), 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.1gives40 * 0 + 42 = 42and4294977127.18.840.113549.1.1.1wraps 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.48and 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