Oct 31, 2026·11 min read·3 visits
Joi versions prior to 17.13.9 and 18.3.1 are vulnerable to Regular Expression Denial of Service (ReDoS) when validating multiple email addresses via Joi.string().email({ multiple: true }). Unauthenticated attackers can send payloads containing long runs of whitespace to trigger quadratic execution time, freezing the Node.js event loop and causing a denial of service.
A Regular Expression Denial of Service (ReDoS) vulnerability exists in the joi schema description and data validation library for JavaScript. When validating multiple email addresses using the Joi.string().email({ multiple: true }) configuration, the library compiles a separator regular expression with an unanchored greedy leading whitespace quantifier. Supplying an input string containing a long sequence of whitespace characters without the expected delimiter triggers quadratic time complexity during String.prototype.split execution, blocking the single-threaded Node.js event loop.
Joi (joi and legacy @hapi/joi) is a widely deployed schema description language and data validator for JavaScript and Node.js applications. It is commonly positioned at the application boundary—such as within Express, Hapi, or Fastify middleware—to validate and sanitize untrusted user input from HTTP requests, query parameters, and JSON payloads before business logic execution.
CVE-2026-118772 (also tracked as GHSA-4528-x5fx-h6hg) is an Inefficient Regular Expression Complexity vulnerability (CWE-1333), commonly referred to as Regular Expression Denial of Service (ReDoS). The flaw resides in the lib/types/string.js implementation of the .email() validation rule when configured with the { multiple: true } option. This option allows a single input string to contain multiple email addresses separated by a default comma (,) or a custom caller-defined delimiter.
To parse individual email addresses from the input string, Joi generates a dynamic regular expression designed to match the delimiter along with any surrounding whitespace. Prior to versions 17.13.9 and 18.3.1, this regular expression utilized an unanchored greedy quantifier (\s*) to match leading whitespace before the separator character. When invoked via String.prototype.split(), this pattern exhibits quadratic time complexity ($\mathcal{O}(N^2)$) if the input contains a long contiguous sequence of whitespace characters without the expected delimiter.
Because the V8 JavaScript engine executes regular expression evaluations synchronously on the main execution thread, triggering this quadratic complexity directly blocks the Node.js event loop. An unauthenticated remote attacker can submit a single HTTP request containing a moderately sized whitespace string (e.g., 100 KB) to an endpoint enforcing this rule, monopolizing CPU resources for several seconds and denying service to all concurrent clients.
The root cause of CVE-2026-118772 is the interaction between an unanchored greedy quantifier in the separator regular expression and the standard ECMAScript execution semantics of String.prototype.split(). When Joi.string().email(options) is initialized in lib/types/string.js, Joi constructs a delimiter pattern using the following expression:
const regex = new RegExp(`\\s*[${options.separator ? escapeRegex(options.separator) : ','}]\\s*`);For the default configuration, this compiles to the regular expression /\s*[,]\s*/. During schema validation of an input string value when options.multiple is enabled, Joi splits the string into candidate addresses by calling value.split(regex).
According to the ECMAScript specification, String.prototype.split(separatorRegExp) scans the target string character-by-character from index $0$ to $N - 1$, attempting to find a match for separatorRegExp at each index. When the input consists of $N$ consecutive space characters (' '.repeat(N)) and contains no comma delimiter, the regular expression engine performs redundant work at every character boundary:
\s* quantifier greedily consumes all $N$ whitespace characters to the end of the string. The engine then attempts to match [,], fails because the end of the string has been reached, and backtracks across all $N$ positions before reporting a match failure at index $0$.split algorithm advances the starting search index to $1$.\s* again consumes the remaining $N - 1$ whitespace characters to the end of the string, fails to find [,], and backtracks.The total number of character comparisons required to complete the split() operation is the sum of the remaining string lengths across all starting positions:
$$\text{Total Comparisons} = \sum_{i=0}^{N-1} (N - i) = \frac{N(N + 1)}{2} = \mathcal{O}(N^2)$$
For an input of $N = 100,000$ spaces, the regex engine performs approximately $5 \times 10^9$ operations. Furthermore, standard schema length restrictions such as Joi.string().max(256).email({ multiple: true }) do not consistently prevent exploitation. If the application invokes validation with { abortEarly: false }—a common pattern used to return all form validation errors to the client simultaneously—Joi records the .max() failure but continues executing subsequent rules in the chain, passing the full oversized string directly into the vulnerable .email() split routine.
The vulnerability was addressed by maintainer Nicolas Morel in commit 03dc1d909818503b06240dadc81e9227ed2649cf for the 18.x branch (PR #3162) and backported to the 17.x branch in commit 807858d7480f5075b0610817e3e44230890fb18e (PR #3163). The modification updates how the separator regular expression is constructed within lib/types/string.js.
--- a/lib/types/string.js
+++ b/lib/types/string.js
@@ -317,7 +317,9 @@ module.exports = Any.extend({
assert(options.multiple === undefined || typeof options.multiple === 'boolean', 'multiple option must be an boolean');
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;
return this.$_addRule({ name: 'email', args: { options }, regex, address });
},The patch introduces two distinct technical changes to eliminate the quadratic complexity and reduce unnecessary object instantiation:
// Before Patch (Vulnerable)
// Matches zero or more whitespace characters from ANY starting position.
const regex = new RegExp(`\\s*[${options.separator ? escapeRegex(options.separator) : ','}]\\s*`);
// After Patch (Fixed)
// 1. Only compiles the regex when options.multiple is true.
// 2. Replaces \s* with (?:(?<!\s)\s+)? using a negative lookbehind assertion.
const regex = options.multiple
? new RegExp(`(?:(?<!\\s)\\s+)?[${options.separator ? escapeRegex(options.separator) : ','}]\\s*`)
: undefined;By replacing the unanchored \s* with the non-capturing group (?:(?<!\s)\s+)?, the regular expression enforces a negative lookbehind assertion (?<!\s) whenever attempting to match leading whitespace. This assertion stipulates that a sequence of one or more whitespace characters (\s+) can only be matched if the character immediately preceding the current match index is not a whitespace character.
When String.prototype.split() evaluates a run of $N$ spaces under the patched regular expression:
(?<!\s) succeeds. The \s+ quantifier consumes the contiguous run of spaces once, checks for the separator, and fails if no separator is present.(?<!\s) fails immediately in $\mathcal{O}(1)$ constant time. The optional group (?:...)? is skipped, and the engine immediately checks whether the character at index $i$ is the separator [,], which also fails in $\mathcal{O}(1)$ time.This prevents the regex engine from re-scanning the remainder of the whitespace run at interior indices, reducing the overall time complexity of value.split(regex) from $\mathcal{O}(N^2)$ to linear $\mathcal{O}(N)$ time. The fix is complete for both default and custom separators, as the lookbehind applies prior to the character class regardless of the delimiter specified in options.separator.
Exploiting CVE-2026-118772 requires no authentication or specialized privileges, provided an attacker can reach an application endpoint that passes user-controlled string input to a Joi schema configured with .email({ multiple: true }). Because the vulnerability relies on missing delimiters rather than specific separator characters, the exact same payload triggers the flaw regardless of whether the target application uses the default comma separator or a custom separator option.
To maximize CPU exhaustion per byte transmitted, the attacker constructs a string composed entirely of whitespace characters (such as ASCII space 0x20 or tab 0x09) without any delimiter characters. If the application enforces .max() length checks on the schema, the attacker can still trigger the vulnerability if the validator is invoked with { abortEarly: false }.
The following proof-of-concept demonstrates the execution time disparity on vulnerable versions (< 17.13.9 and >= 18.0.0, < 18.3.1), including the bypass of .max() when abortEarly: false is configured:
const Joi = require('joi');
// Schema with a length limit of 256 characters and multiple email validation
const schema = Joi.string().max(256).email({ multiple: true });
// Generate a 100,000-byte whitespace payload containing no separator
const payload = ' '.repeat(100000);
console.log(`Testing payload of ${payload.length} bytes...`);
const startTime = process.hrtime.bigint();
// Validate with abortEarly: false, which evaluates .email() even after .max(256) fails
const { error } = schema.validate(payload, { abortEarly: false });
const endTime = process.hrtime.bigint();
const durationMs = Number(endTime - startTime) / 1e6;
console.log(`Validation finished in ${durationMs.toFixed(2)} ms`);
console.log(`Validation error count: ${error ? error.details.length : 0}`);On vulnerable releases (joi@17.13.8 or joi@18.3.0), executing this script blocks the Node.js process for multiple seconds on a single 100 KB string. Because HTTP body parsers (such as express.json() or body-parser) typically allow default payload sizes between 100 KB and 1 MB, an attacker can send a small number of concurrent requests to continuously saturate the single-threaded event loop. On patched versions (17.13.9 and 18.3.1), the same validation completes in under 20 milliseconds.
The primary impact of CVE-2026-118772 is a high-severity loss of service availability, reflected in its CVSS v3.1 score of 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H). The vulnerability does not allow unauthorized disclosure of information (Confidentiality: None) or modification of application data (Integrity: None), and its scope is constrained to the vulnerable application process (Scope: Unchanged).
In the Node.js runtime architecture, all incoming network connections, I/O callbacks, and timer events are multiplexed onto a single main execution thread managed by libuv. Synchronous, CPU-bound operations—such as V8 regular expression evaluation—prevent the event loop from advancing to subsequent phases until the synchronous call returns.
When an attacker submits a request containing a long whitespace string to an endpoint using Joi.string().email({ multiple: true }), the entire Node.js worker process becomes unresponsive for the duration of the split() call. During this window, the server cannot accept new TCP connections, process health check probes, or respond to existing requests from legitimate users. In containerized environments (such as Kubernetes or AWS ECS), sustained event loop blockage often causes liveness probes to time out, triggering cascading container restarts and cluster-wide instability.
According to FIRST Exploit Prediction Scoring System (EPSS) metrics, CVE-2026-118772 holds an EPSS score of 0.00467 (0.47%), placing it in the 40.46th percentile of scored vulnerabilities. The vulnerability is not currently listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.
To remediate CVE-2026-118772, development and operations teams must upgrade the joi package to a patched release that incorporates the negative lookbehind fix:
joi@17.13.9 or later.joi@18.3.1 or later.@hapi/joi package namespace: Migrate to the modern joi package at version 17.13.9 or 18.3.1, as @hapi/joi no longer receives security updates.> [!NOTE]
> Because the patched regular expression utilizes a negative lookbehind assertion ((?<!\s)), browser-bundled builds of Joi require environments supporting ECMAScript 2018 regular expression lookbehinds (such as Safari 16.4+, Chrome 62+, Firefox 78+, and all modern Node.js runtimes). Joi only instantiates this regular expression when { multiple: true } is explicitly configured.
In environments where an immediate dependency upgrade cannot be deployed, the following workarounds mitigate the risk of event loop exhaustion:
{ multiple: true } with Array Validation: Refactor schemas to accept an array of individual email strings rather than a single delimited string. This completely avoids the vulnerable separator regular expression:
// Vulnerable configuration
const vulnerableSchema = Joi.string().email({ multiple: true });
// Safe alternative using array items
const safeSchema = Joi.array().items(Joi.string().email().max(254)).max(50);if (typeof inputEmailString === 'string' && inputEmailString.length > 1000) {
throw new Error('Input exceeds maximum allowed length');
}abortEarly: true on Untrusted Input: When combining .max() with .email({ multiple: true }), ensure that abortEarly is set to true (the default behavior) and that .max() is declared prior to .email() in the rule chain so validation halts immediately upon encountering an oversized string.CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
joi hapijs | >= 17.1.1, < 17.13.9 | 17.13.9 |
joi hapijs | >= 18.0.0, < 18.3.1 | 18.3.1 |
@hapi/joi hapijs | <= 17.1.1 | 17.13.9 (via migration to joi) |
| Attribute | Detail |
|---|---|
| Vulnerability Class | Regular Expression Denial of Service (CWE-1333) |
| CVSS v3.1 Score | 7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) |
| Attack Vector | Network (Unauthenticated HTTP Request) |
| Affected Component | Joi.string().email({ multiple: true }) in lib/types/string.js |
| Security Impact | Node.js Event Loop Exhaustion / Remote Denial of Service |
| EPSS Score | 0.47% (40.46th Percentile) |
| Exploit Status | Proof of Concept Available |
| Patched Versions | 17.13.9, 18.3.1 |
The software uses a regular expression that can take an exponential or polynomial amount of time relative to the size of the input, leading to excessive consumption of CPU resources (ReDoS).
A regular expression denial of service (ReDoS) vulnerability exists in @hapi/content from version 3.0.6 prior to 6.0.3. The Content.disposition() function utilizes an unanchored parameter-name regular expression inside a global replacement scan. When processing a Content-Disposition header parameter containing a long token lacking an equals sign, the JavaScript regular expression engine undergoes quadratic O(N^2) backtracking. Because @hapi/content is invoked by @hapi/pez during multipart/form-data request parsing, unauthenticated remote attackers can send a crafted multipart request to any Hapi route with multipart payload parsing enabled, starving the single-threaded Node.js event loop.
CVE-2026-119205 (GHSA-mfr5-77xh-9j7h) is a Denial of Service (DoS) vulnerability in pypdf prior to version 6.21.4. The flaw occurs during the cloning or duplication of nested PDF dictionary structures. Because the cycle-detection algorithm within DictionaryObject._clone() tracked generated destination object references rather than the original source indirect references, cyclic reference graphs in source PDFs evaded detection, resulting in an infinite loop and CPU exhaustion.
CVE-2026-119204 (also tracked as GHSA-cggf-277g-3q54) is a Denial of Service (DoS) vulnerability in pypdf prior to version 6.21.4. The vulnerability stems from uncontrolled resource consumption (CWE-400) during the flattening of a PDF document's page tree. By supplying a compact PDF containing a hierarchical amplification structure of shared `/Pages` and `/Page` nodes with dense dictionary entries or inherited resources, an unauthenticated attacker can cause pypdf to copy millions of dictionary entries into individual `PageObject` instances without enforcing a document-wide entry budget. This triggers severe heap memory exhaustion and process termination via Out-Of-Memory (OOM) kills.
CVE-2026-119203 is a denial-of-service vulnerability affecting the pypdf library in versions prior to 6.21.4. The flaw occurs within the standard cross-reference (xref) table parsing routine when processing truncated or malformed PDF streams that terminate before a valid trailer dictionary is encountered. Because the parser attempts to rewind the stream pointer by a fixed offset whenever the trailer token is absent without verifying forward progress, hitting the end-of-file (EOF) or non-advancing tokens causes the parser to re-read the same byte offset indefinitely. An unauthenticated attacker can exploit this flaw by supplying a crafted PDF document to an application using pypdf, resulting in continuous CPU consumption and thread exhaustion.
Prior to version 2.18.0, the Kubeflow Pipelines API server contains a missing authorization and confused deputy vulnerability in its run log streaming endpoint. The HTTP handler for `GET /apis/v1alpha1/runs/{run_id}/nodes/{node_id}/log` fails to propagate incoming request headers to gRPC metadata and omits caller authorization checks against the requested run. Furthermore, the backend accepts the caller-supplied `node_id` parameter directly as a Kubernetes pod name without verifying that the pod belongs to the specified pipeline run. An attacker with network access to the API server who knows a valid run identifier can read live or archived execution logs from any pod containing a `main` container within the run's Kubernetes namespace.
A high-severity Denial of Service (DoS) vulnerability exists in Kubeflow Pipelines prior to version 2.18.0 due to improper handling of highly compressed archive files (CWE-409). The API server decompresses uploaded ZIP and gzip-compressed TAR archives directly into memory without bounding the expanded byte stream or archive traversal metadata. Because decompression occurs before namespace-level authorization in multipart upload handlers, an unauthenticated or unauthorized attacker with network access to the API server can exhaust container heap memory and trigger Out-Of-Memory (OOM) terminations.