high

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:

  1. node-forge < 1.4.1 (1.4.2 recommended, 17 CVEs): Digital Bazaar’s pure-JavaScript TLS, X.509, PKCS#12, and ASN.1 toolkit (~35M weekly npm downloads) shipped 1.4.1 and 1.4.2 to 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() and pkcs12.pkcs12FromAsn1() accepted an iteration count of 0 (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/.pfx file (replacing certificates or encrypted private keys) and compute a valid MAC that verifies under any password.
    • X.509 basicConstraints CA spoofing via deprecated OIDs (CVE-2026-119190 / GHSA-5x38-2vff-x55p, CVSS 7.4 High): node-forge mapped two obsolete X.509 OIDs (2.5.29.10 and 2.5.29.13) to the same extension name 'basicConstraints' as the standard RFC 5280 OID (2.5.29.19), and pki.verifyCertificateChain() looked up cert.getExtension('basicConstraints'), which returned the first matching extension. If a CA copied unrecognized CSR extensions into an issued leaf certificate before 2.5.29.19, placing {id: '2.5.29.10', cA: true} first caused node-forge to trust the leaf certificate as an intermediate CA.
    • RSA-PKCS#1 v1.5 e=3 signature forgery (CVE-2026-85393 / GHSA-2g46-22mc-6gwp and CVE-2026-119192 / GHSA-6qrw-m8cv-hcxf): Incomplete DigestInfo validation from CVE-2026-33894 still allowed trailing garbage inside the nested DigestAlgorithm SEQUENCE or non-canonical BER length/OID encodings, enabling Bleichenbacher signature forgery against e=3 RSA keys.
    • ASN.1 BIT STRING maxDepth bypass (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-119187 and CVE-2026-119188, both CVSS 7.5 High): asn1.fromDer() recursively decoded nested BIT STRING payloads without forwarding options.maxDepth (an incomplete fix for CVE-2025-66031); pem.decode() stalled for >20s on a 30 KB PEM string missing its -----END line; and tls.createConnection() crashed the Node.js process with RangeError or TypeError when sent 50,000 tiny heartbeat records in one chunk or a single non-handshake record interleaved inside a fragmented handshake.
  2. joi 17.1.1 to < 17.13.9 and 18.0.0 to < 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.
  3. @hapi/content 3.0.6 to < 6.0.3 (CVE-2026-119202 / GHSA-8494-f6mr-cjpv, CVSS 7.5 High): Content.disposition() used an unanchored global regex (internals.contentDispositionParamRegex) to parse Content-Disposition parameters, causing quadratic backtracking whenever a multipart/form-data part header contained a long token without =.
  4. nuxt 4.6.0 to < 4.6.3 (CVE-2026-119200 / GHSA-9v7c-q64q-vv33, CVSS 6.5 Medium, plus CVE-2026-119198, CVE-2026-119199, and CVE-2026-119201): In Nuxt 4.6.0 through 4.6.2, Nitro routing, server/middleware, and routeRules evaluated the raw path (such as /%2e/admin), while the Vue SSR page renderer resolved dot segments and rendered app/pages/admin.vue along 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

EcosystemPackageCVE / GHSASeverityAffected versionsFixed versionVulnerable component
npmnode-forgeCVE-2026-118782 (GHSA-jc66-7q78-782g), CVE-2026-119190 (GHSA-5x38-2vff-x55p), CVE-2026-85393, CVE-2026-119192CVSS 7.5 / 7.4 High< 1.4.11.4.2 (1.4.1)lib/pbe.js, lib/pkcs12.js, lib/x509.js, lib/rsa.js
npmnode-forgeCVE-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 CVEsCVSS 7.5 High to 2.9 Low< 1.4.11.4.2 (1.4.1)lib/asn1.js, lib/tls.js, lib/pem.js, lib/cipherModes.js
npmjoiCVE-2026-118772 (GHSA-4528-x5fx-h6hg)CVSS 7.5 High>= 17.1.1, < 17.13.9; >= 18.0.0, < 18.3.117.13.9, 18.3.1lib/types/string.js (Joi.string().email({ multiple: true }))
npm@hapi/contentCVE-2026-119202 (GHSA-8494-f6mr-cjpv)CVSS 7.5 High>= 3.0.6, < 6.0.36.0.3lib/index.js (Content.disposition)
npmnuxtCVE-2026-119200 (GHSA-9v7c-q64q-vv33), CVE-2026-119198, CVE-2026-119199, CVE-2026-119201CVSS 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
  1. Upgrade node-forge to >= 1.4.2: Version 1.4.2 includes all 17 security fixes from 1.4.1 plus the fix for #1165 (TypeError when Buffer inputs are passed to pem.decode() by downstream libraries such as node-jose).
  2. Upgrade joi to >= 17.13.9 or >= 18.3.1 and @hapi/content to >= 6.0.3.
  3. Upgrade nuxt to >= 4.6.3 (or >= 3.21.11 on Nuxt 3): In addition to CVE-2026-119200, 4.6.3 fixes dev-server exposures (CVE-2026-119198, CVE-2026-119199, and CVE-2026-119201).

Timeline

  • 2026-10-21 to 2026-10-28: Maintainers commit fixes in @hapi/content (0993851dc7, v6.0.3), node-forge (v1.4.1 and v1.4.2 on 27 October), nuxt (45463f8b58, v4.6.3 on 27 October), and joi (03dc1d9098, v17.13.9 / v18.3.1 on 28 October).
  • 2026-10-29 (19:17 to 21:17 UTC): NVD and the GitHub Advisory Database publish the 15 new node-forge CVEs (CVE-2026-118774..118782 and CVE-2026-119186..119191, joining CVE-2026-85393 and CVE-2026-119192), CVE-2026-118772 (joi), CVE-2026-119202 (@hapi/content), and CVE-2026-119198 through CVE-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