CVE
CVE-2026-118782, CVE-2026-119190, CVE-2026-85393, CVE-2026-119192, CVE-2026-119191, CVE-2026-119187, CVE-2026-119188, CVE-2026-118779, CVE-2026-119189, CVE-2026-119186, CVE-2026-118778, CVE-2026-118772, CVE-2026-119202, CVE-2026-119200, CVE-2026-119198, CVE-2026-119199, CVE-2026-119201
CWE
CWE-347, CWE-295, CWE-674, CWE-1333, CWE-400, CWE-22
Affected Surface
- npm `node-forge` versions `< 1.4.1` (upgrade to `1.4.2`, which fixes a `Buffer` regression in `pem.decode()` from `1.4.1`) across 17 CVEs (`CVE-2026-118782`, `CVE-2026-119190`, `CVE-2026-85393`, `CVE-2026-119192`, `CVE-2026-119191`, `CVE-2026-119187`, `CVE-2026-119188`, `CVE-2026-118779`, `CVE-2026-119189`, `CVE-2026-119186`, and `CVE-2026-118774` through `CVE-2026-118781`)
- npm `joi` versions `>= 17.1.1, < 17.13.9` and `>= 18.0.0, < 18.3.1` (`CVE-2026-118772` / `GHSA-4528-x5fx-h6hg`, CVSS 7.5 High)
- npm `@hapi/content` versions `>= 3.0.6, < 6.0.3` (`CVE-2026-119202` / `GHSA-8494-f6mr-cjpv`, CVSS 7.5 High)
- npm `nuxt` versions `>= 4.6.0, < 4.6.3` (`CVE-2026-119200` / `GHSA-9v7c-q64q-vv33`, `CVE-2026-119198`, and `CVE-2026-119201`) and `>= 3.0.0, < 4.6.3` (`CVE-2026-119199`)
TL;DR
Late on 29 October 2026 (19:17 to 21:17 UTC), NVD and the GitHub Advisory Database published 22 security advisories across four heavily downloaded JavaScript and TypeScript libraries:
node-forge< 1.4.1(1.4.2recommended, 17 CVEs): Digital Bazaar’s pure-JavaScript TLS, X.509, PKCS#12, and ASN.1 toolkit (~35M weekly npm downloads) shipped1.4.1and1.4.2to patch eight High-severity and nine Moderate/Low vulnerabilities:- PKCS#12 MAC and encryption bypass via
iter <= 0(CVE-2026-118782/GHSA-jc66-7q78-782g, CVSS 7.5 High):pki.pbe.generatePkcs12Key()andpkcs12.pkcs12FromAsn1()accepted an iteration count of0(or negative). Under RFC 7292 Appendix B.2, zero hash iterations skip the password and salt loop completely and return a constant all-zero derived key, letting an attacker who does not know the password tamper with a.p12/.pfxfile (replacing certificates or encrypted private keys) and compute a valid MAC that verifies under any password. - X.509
basicConstraintsCA spoofing via deprecated OIDs (CVE-2026-119190/GHSA-5x38-2vff-x55p, CVSS 7.4 High):node-forgemapped two obsolete X.509 OIDs (2.5.29.10and2.5.29.13) to the same extension name'basicConstraints'as the standard RFC 5280 OID (2.5.29.19), andpki.verifyCertificateChain()looked upcert.getExtension('basicConstraints'), which returned the first matching extension. If a CA copied unrecognized CSR extensions into an issued leaf certificate before2.5.29.19, placing{id: '2.5.29.10', cA: true}first causednode-forgeto trust the leaf certificate as an intermediate CA. - RSA-PKCS#1 v1.5
e=3signature forgery (CVE-2026-85393/GHSA-2g46-22mc-6gwpandCVE-2026-119192/GHSA-6qrw-m8cv-hcxf): IncompleteDigestInfovalidation fromCVE-2026-33894still allowed trailing garbage inside the nestedDigestAlgorithmSEQUENCEor non-canonical BER length/OID encodings, enabling Bleichenbacher signature forgery againste=3RSA keys. - ASN.1
BIT STRINGmaxDepthbypass (CVE-2026-119191/GHSA-rhr8-3pm8-cq29, CVSS 7.5 High), PEM ReDoS (CVE-2026-118779, CVSS 7.5 High), and pre-handshake TLS process crashes (CVE-2026-119187andCVE-2026-119188, both CVSS 7.5 High):asn1.fromDer()recursively decoded nestedBIT STRINGpayloads without forwardingoptions.maxDepth(an incomplete fix forCVE-2025-66031);pem.decode()stalled for >20s on a 30 KB PEM string missing its-----ENDline; andtls.createConnection()crashed the Node.js process withRangeErrororTypeErrorwhen sent 50,000 tiny heartbeat records in one chunk or a single non-handshake record interleaved inside a fragmented handshake.
- PKCS#12 MAC and encryption bypass via
joi17.1.1to< 17.13.9and18.0.0to< 18.3.1(CVE-2026-118772/GHSA-4528-x5fx-h6hg, CVSS 7.5 High):Joi.string().email({ multiple: true })split input on/\s*[,]\s*/, whose unanchored leading\s*retried at every character of a long whitespace run without a comma. Notably, chaining.max(254)did not stop the ReDoS when callers validated with{ abortEarly: false }, because Joi still executed the email rule after.max()failed.@hapi/content3.0.6to< 6.0.3(CVE-2026-119202/GHSA-8494-f6mr-cjpv, CVSS 7.5 High):Content.disposition()used an unanchored global regex (internals.contentDispositionParamRegex) to parseContent-Dispositionparameters, causing quadratic backtracking whenever amultipart/form-datapart header contained a long token without=.nuxt4.6.0to< 4.6.3(CVE-2026-119200/GHSA-9v7c-q64q-vv33, CVSS 6.5 Medium, plusCVE-2026-119198,CVE-2026-119199, andCVE-2026-119201): In Nuxt4.6.0through4.6.2, Nitro routing,server/middleware, androuteRulesevaluated the raw path (such as/%2e/admin), while the Vue SSR page renderer resolved dot segments and renderedapp/pages/admin.vuealong with its server-fetched data, bypassing path-scoped auth middleware and security headers.
What to do now: Upgrade node-forge to >= 1.4.2 (do not stop at 1.4.1, which broke Buffer inputs to pem.decode() used by node-jose), joi to >= 17.13.9 or >= 18.3.1, @hapi/content to >= 6.0.3, and nuxt to >= 4.6.3 (or >= 3.21.11 on Nuxt 3).
Affected packages
| Ecosystem | Package | CVE / GHSA | Severity | Affected versions | Fixed version | Vulnerable component |
|---|---|---|---|---|---|---|
| npm | node-forge | CVE-2026-118782 (GHSA-jc66-7q78-782g), CVE-2026-119190 (GHSA-5x38-2vff-x55p), CVE-2026-85393, CVE-2026-119192 | CVSS 7.5 / 7.4 High | < 1.4.1 | 1.4.2 (1.4.1) | lib/pbe.js, lib/pkcs12.js, lib/x509.js, lib/rsa.js |
| npm | node-forge | CVE-2026-119191 (GHSA-rhr8-3pm8-cq29), CVE-2026-119187 (GHSA-hqq3-7jhf-c5jx), CVE-2026-119188 (GHSA-8hrf-8g6f-7xwp), CVE-2026-118779 (GHSA-7wch-p9px-956x), plus 9 Moderate/Low CVEs | CVSS 7.5 High to 2.9 Low | < 1.4.1 | 1.4.2 (1.4.1) | lib/asn1.js, lib/tls.js, lib/pem.js, lib/cipherModes.js |
| npm | joi | CVE-2026-118772 (GHSA-4528-x5fx-h6hg) | CVSS 7.5 High | >= 17.1.1, < 17.13.9; >= 18.0.0, < 18.3.1 | 17.13.9, 18.3.1 | lib/types/string.js (Joi.string().email({ multiple: true })) |
| npm | @hapi/content | CVE-2026-119202 (GHSA-8494-f6mr-cjpv) | CVSS 7.5 High | >= 3.0.6, < 6.0.3 | 6.0.3 | lib/index.js (Content.disposition) |
| npm | nuxt | CVE-2026-119200 (GHSA-9v7c-q64q-vv33), CVE-2026-119198, CVE-2026-119199, CVE-2026-119201 | CVSS 6.5 Medium to 4.8 Medium | >= 4.6.0, < 4.6.3 (119199 affects >= 3.0.0, < 4.6.3) | 4.6.3 (3.21.11 for 3.x) | packages/nitro-server/src/runtime/utils/event.ts (encodeEventPath) |
Technical analysis
1. node-forge 1.4.1 / 1.4.2: PKCS#12 iter <= 0 constant-key forgery (CVE-2026-118782), deprecated-OID basicConstraints bypass (CVE-2026-119190), and BIT STRING maxDepth bypass (CVE-2026-119191)
In lib/pbe.js and lib/pkcs12.js, pkcs12.pkcs12FromAsn1(obj, strict, password) parses the ASN.1 MacData structure from a PKCS#12 / PFX container, reads macIterations, and derives the HMAC key via pki.pbe.generatePkcs12Key(password, salt, id, iter, n, md).
Under RFC 7292 Appendix B.2, the PKCS#12 key-derivation loop hashes the concatenated diverter, salt, and UTF-16BE password iter times:
// lib/pbe.js in node-forge < 1.4.1 (vulnerable when iter <= 0)
for(var count = 0; count < iter; count++) {
md.start();
md.update(ai);
ai = md.digest().getBytes();
}
When an attacker set macIterations = 0 in the ASN.1 MacData block (or iterations = 0 in a pbeWithSHAAnd3-KeyTripleDES-CBC / pbewithSHAAnd40BitRC2-CBC encrypted key bag), the for (var count = 0; count < iter; count++) loop executed zero times. The derived key ai never incorporated the password or salt at all, leaving a fixed, password-independent key. Any attacker could replace the certificates or shrouded private keys inside a .p12 file, set macIterations = 0, and compute a MAC that pkcs12FromAsn1() accepted for any password supplied by the victim application. Commit d9584aa568 rejects any PKCS#12 PBE or MAC iteration count < 1:
--- a/lib/pbe.js
+++ b/lib/pbe.js
@@ -642,6 +642,12 @@ pki.pbe.generatePkcs12Key = function(password, salt, id, iter, n, md) {
var j, l;
+
+ // RFC 7292 B.2: with fewer than 1 iteration no hashing is done and the
+ // derived key would be a constant that does not depend on the password
+ if(!(iter >= 1)) {
+ throw new Error('PKCS#12 key derivation iteration count must be >= 1.');
+ }
In lib/x509.js (CVE-2026-119190 / GHSA-5x38-2vff-x55p, reported by the WorkOS Security Team), node-forge’s OID table mapped three OIDs to the name 'basicConstraints': the standard X.509v3 OID 2.5.29.19 (id-ce-basicConstraints) and two deprecated X.509 OIDs (2.5.29.10 and 2.5.29.13). During chain verification, pki.verifyCertificateChain() looked up the extension by name rather than OID:
--- a/lib/x509.js
+++ b/lib/x509.js
@@ -3149,7 +3149,9 @@ pki.verifyCertificateChain = function(caStore, chain, options) {
if(error === null &&
(!first || (chain.length === 0 && (!parent || selfSigned)))) {
// first check keyUsage extension and then basic constraints
- var bcExt = cert.getExtension('basicConstraints');
+ // (by id: deprecated OIDs are also named 'basicConstraints', but only
+ // id-ce-basicConstraints 2.5.29.19 is valid, RFC 5280 4.2.1.9)
+ var bcExt = cert.getExtension({id: '2.5.29.19'});
Because cert.getExtension('basicConstraints') returned the first extension in the certificate whose mapped name equaled 'basicConstraints', a certificate containing a non-critical {id: '2.5.29.10', cA: true} extension placed ahead of the real {id: '2.5.29.19', cA: false} extension was treated as a valid CA certificate (provided the CA either omitted keyUsage or included keyCertSign), and the same trick bypassed pathLenConstraint.
Similarly, in lib/asn1.js (CVE-2026-119191 / GHSA-rhr8-3pm8-cq29, commit 24035c181f), when asn1.fromDer() speculatively decoded the inner bytes of a BIT STRING as embedded ASN.1 (decodeBitStrings: true), it constructed var subOptions = { strict: true, decodeBitStrings: true } without copying maxDepth: options.maxDepth. Nesting BIT STRING values inside BIT STRING values bypassed CVE-2025-66031’s depth limit and allocated quadratic copies of the buffer at each depth level until V8 ran out of heap memory.
2. joi 17.13.9 / 18.3.1 (CVE-2026-118772) and @hapi/content 6.0.3 (CVE-2026-119202): quadratic separator and Content-Disposition regexes
In joi (lib/types/string.js), enabling Joi.string().email({ multiple: true }) splits a comma-separated (or custom-delimited) email list using a precompiled RegExp. Before commit 03dc1d9098, the separator regex began with an unanchored \s*:
--- a/lib/types/string.js
+++ b/lib/types/string.js
@@ -317,7 +317,9 @@ module.exports = Any.extend({
const address = internals.addressOptions(options);
- const regex = new RegExp(`\\s*[${options.separator ? escapeRegex(options.separator) : ','}]\\s*`);
+ // Only start the leading whitespace at the beginning of a run, otherwise the split is quadratic on long whitespace runs,
+ // and only build it when needed since lookbehind requires Safari 16.4+
+ const regex = options.multiple ? new RegExp(`(?:(?<!\\s)\\s+)?[${options.separator ? escapeRegex(options.separator) : ','}]\\s*`) : undefined;
When an input contained a long run of spaces not followed by a comma (for example ' '.repeat(100000)), String.prototype.split(regex) matched \s* from index 0 to the end of the run, failed to find ,, advanced to index 1, matched \s* to the end of the run again, and repeated $O(n^2)$ times. Worse, applications that validate schemas with .validate(input, { abortEarly: false }) run all string rules even after .max(254) fails, so a .max() rule on the same schema did not prevent the event-loop freeze.
In @hapi/content (lib/index.js, commit 0993851dc7), Content.disposition() parsed Content-Disposition: form-data; ... headers with a /g regex whose first group ([^\=\*\s]+)(\*)?\s*\=\s*... required an = sign. Sending form-data; name="f"; aaaaa... (64,000 a characters with no =) forced the global regex engine to retry ([^\=\*\s]+) from every character offset in the 64 KB token before @hapi/pez or route validation ever ran.
3. nuxt 4.6.3 (CVE-2026-119200 / GHSA-9v7c-q64q-vv33): dot-segment normalization discrepancy between Nitro and Vue Router
In Nuxt 4.6.0 through 4.6.2, incoming HTTP requests first pass through Nitro’s router, routeRules (which apply redirects, CORS, auth headers, and caching rules), and server/middleware handlers using event.path. When the request fell through to the Nuxt SSR renderer, encodeEventPath(path) in packages/nitro-server/src/runtime/utils/event.ts passed the path to new URL(path, 'http://localhost') and read .pathname:
--- a/packages/nitro-server/src/runtime/utils/event.ts
+++ b/packages/nitro-server/src/runtime/utils/event.ts
@@ -9,6 +9,7 @@ const ENC_BRACKET_CLOSE_RE = /%5D/g
const ENC_ENC_SLASH_RE = /%252F/gi
const HASH_RE = /#/g
const QUESTION_MARK_RE = /\?/g
+const DOT_SEGMENT_RE = /(?:^|\/)\.{1,2}(?:\/|$)/
Because the WHATWG URL constructor collapses /./ and /../ dot segments (and h3 decodes %2e to . before calling encodeEventPath), a request to GET /%2e/admin or GET /public/../admin did not match a Nitro routeRules['/admin/**'] or a server/middleware check guarding event.path.startsWith('/admin'), yet encodeEventPath() normalized it to /admin and rendered app/pages/admin.vue along with its server-side data. Commit 45463f8b58 (v4.6.3) rejects paths matching DOT_SEGMENT_RE before rendering.
Detection and remediation
# 1. Check Node.js lockfiles and dependency trees for vulnerable versions
npm ls node-forge joi @hapi/content nuxt 2>/dev/null || true
rg -n '"(node-forge|joi|@hapi/content|nuxt)"' \
package.json package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null
# 2. Search for Joi.string().email({ multiple: true }) or node-forge pkcs12FromAsn1 / verifyCertificateChain
rg -n 'email\s*\(\s*\{[^}]*multiple\s*:\s*true|pkcs12FromAsn1|verifyCertificateChain' \
--glob '*.{js,ts,mjs,cjs}' . 2>/dev/null
- Upgrade
node-forgeto>= 1.4.2: Version1.4.2includes all 17 security fixes from1.4.1plus the fix for#1165(TypeErrorwhenBufferinputs are passed topem.decode()by downstream libraries such asnode-jose). - Upgrade
joito>= 17.13.9or>= 18.3.1and@hapi/contentto>= 6.0.3. - Upgrade
nuxtto>= 4.6.3(or>= 3.21.11on Nuxt 3): In addition toCVE-2026-119200,4.6.3fixes dev-server exposures (CVE-2026-119198,CVE-2026-119199, andCVE-2026-119201).
Timeline
- 2026-10-21 to 2026-10-28: Maintainers commit fixes in
@hapi/content(0993851dc7,v6.0.3),node-forge(v1.4.1andv1.4.2on 27 October),nuxt(45463f8b58,v4.6.3on 27 October), andjoi(03dc1d9098,v17.13.9/v18.3.1on 28 October). - 2026-10-29 (
19:17to21:17 UTC): NVD and the GitHub Advisory Database publish the 15 newnode-forgeCVEs (CVE-2026-118774..118782andCVE-2026-119186..119191, joiningCVE-2026-85393andCVE-2026-119192),CVE-2026-118772(joi),CVE-2026-119202(@hapi/content), andCVE-2026-119198throughCVE-2026-119201(nuxt).
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.
References
- node-forge v1.4.1 / v1.4.2 CHANGELOG and commits d9584aa568, 58c06bc251, 24035c181f, and 817e2ce0d0
- node-forge advisories GHSA-jc66-7q78-782g (CVE-2026-118782), GHSA-5x38-2vff-x55p (CVE-2026-119190), and GHSA-rhr8-3pm8-cq29 (CVE-2026-119191)
- hapijs/joi advisory GHSA-4528-x5fx-h6hg (CVE-2026-118772) and commit 03dc1d9098
- hapijs/content advisory GHSA-8494-f6mr-cjpv (CVE-2026-119202) and commit 0993851dc7
- nuxt/nuxt advisory GHSA-9v7c-q64q-vv33 (CVE-2026-119200) and commit 45463f8b58