Feb 19, 2026·5 min read·12 visits
The `jsdiff` library contains a Regex Denial of Service (ReDoS) and infinite loop vulnerability. The regex used to parse diff headers treats the dot `.` character as 'anything but newline', failing to account for Unicode line separators. This causes the parser to get stuck reprocessing the same line forever. Update to versions 3.5.1, 4.0.4, 5.2.2, or 8.0.3 immediately.
A denial-of-service vulnerability exists in the ubiquitous `jsdiff` library, used by millions of Node.js applications for text comparison. The flaw lies within the `parsePatch` method, where an insecure regular expression fails to handle specific Unicode line terminators. By crafting a patch file with a `\u2028` (Line Separator) or `\u2029` (Paragraph Separator) in the filename header, an attacker can force the parser into an infinite loop, starving the Node.js event loop and crashing the application.
If you write JavaScript, you use jsdiff. You might not know it, but you do. It's the silent workhorse behind mocha, karma, standard git GUIs, and pretty much every testing framework that shows you a colorful diff when your assertions fail. It is the plumbing of the JS ecosystem.
But plumbing is only exciting when it leaks. In this case, jsdiff isn't leaking water; it's leaking CPU cycles. Specifically, in the parsePatch and applyPatch methods—the functions responsible for taking a unified diff string and applying it to code.
Imagine a scenario where a user submits a patch file to your code review tool, or a logging system parses a diff for display. If that text contains a specific, invisible Unicode character, the entire Node.js process—single-threaded as it is—will freeze instantly. It won't throw an error. It won't timeout. It will simply stare at that line of text until the heat death of the universe (or until your watchdog kills the container).
The vulnerability is a classic case of "Regular Expressions are harder than you think." The root cause is how JavaScript's regex engine handles the dot (.) character versus how the developer thought it handled it.
In the parsePatch function, the library attempts to identify file headers (lines starting with --- or +++). The developers used this regex:
(/^(---|\+\+\+)\s+(.*)\r?$/)The intention is clear: match the prefix, some whitespace, and then (.*) captures the rest of the line (the filename). The assumption was that . matches everything up to the end of the line. But in JavaScript, . does not match line terminators. We all know it doesn't match \n. But did you know it also doesn't match \u2028 (Line Separator) or \u2029 (Paragraph Separator)?
When the parser encounters a line like --- filename\u2028.txt, the regex engine sees the ---, matches the space, but then (.*) hits the \u2028 and stops. Because the regex is anchored with $ (end of string), the entire match fails.
The parser logic, expecting a header but failing to match one, doesn't advance its internal pointer correctly. It circles back, sees the --- again, tries the regex again, fails again, and enters an infinite loop. It is the software equivalent of a dog chasing its tail, except the dog consumes 100% of your vCPU.
The fix is a beautiful example of "boring is better." The maintainers realized that using a complex regex to parse a simple string prefix is asking for trouble. They abandoned the capture group entirely in favor of raw string manipulation.
Here is the vulnerable code logic compared to the patched version in commit 15a1585230748c8ae6f8274c202e0c87309142f5:
Before (The CPU Eater):
// Relies on the greedy (.*) which fails on unicode terminators
const fileHeader = (/^(---|\+\+\+)\s+(.*)\r?$/).exec(diffstr[i]);
if (fileHeader) {
// Extract using regex capture group
const data = fileHeader[2].split('\t', 2);
// ...
}After (The Safe Approach):
// Only checks the prefix. Doesn't care what comes after yet.
const fileHeaderMatch = (/^(---|\+\+\+)\s+/).exec(diffstr[i]);
if (fileHeaderMatch) {
const prefix = fileHeaderMatch[1];
// Manually slice the string. substring() doesn't care about unicode.
const data = diffstr[i].substring(3).trim().split('\t', 2);
// ...
}By switching to substring(3), the code explicitly says "give me everything after the third character." This method is agnostic to line terminators. Whether it's a standard newline or a weird paragraph separator, substring eats it all. It's dumber, faster, and infinitely safer.
Exploiting this is trivially easy if you have an injection vector where an application parses a patch file. You don't need shellcode. You don't need memory addresses. You just need a text editor that supports Unicode insertion.
The Attack Scenario:
jsdiff to display changes.U+2028).The PoC:
const Diff = require('diff');
// The \u2028 character is the "poison pill"
const maliciousPatch =
'--- malicious_file\u2028name.js\n' +
'+++ malicious_file\u2028name.js\n' +
'@@ -1 +1 @@\n' +
'-old\n' +
'+new';
console.log("Starting parse... (Say goodbye to your CPU)");
// This line will hang forever
Diff.applyPatch('old content', maliciousPatch);When applyPatch runs, the internal parser gets stuck on the first line. The application will hang. If this is running in a web request handler, the request will timeout, but the Node process will remain pegged at 100% CPU usage until the OS kills it.
While the CVSS score is technically low (2.7) due to the complexity of the attack requirements (you need to feed a patch file to the system), the impact on availability is absolute. In the Node.js event loop model, a synchronous infinite loop is the kiss of death.
Why it hurts:
This is a stark reminder that input validation isn't just about stopping SQL injection; it's about ensuring your parser actually survives the input it's given.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:U| Product | Affected Versions | Fixed Version |
|---|---|---|
jsdiff kpdecker | < 3.5.1 | 3.5.1 |
jsdiff kpdecker | >= 4.0.0, < 4.0.4 | 4.0.4 |
jsdiff kpdecker | >= 5.0.0, < 5.2.2 | 5.2.2 |
jsdiff kpdecker | >= 6.0.0, < 8.0.3 | 8.0.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-400 (Resource Consumption) |
| Secondary CWE | CWE-1333 (Inefficient Regex) |
| CVSS v4.0 | 2.7 (Low) |
| Attack Vector | Network |
| EPSS Score | 0.00018 (~4%) |
| Affected Component | parsePatch / applyPatch |
Uncontrolled Resource Consumption