CVE-2026-24001

Diffing Dangerously: Infinite Loops and ReDoS in jsdiff

Alon Barad
Alon Barad
Software Engineer

Jan 22, 2026·7 min read·3 visits

Executive Summary (TL;DR)

The `jsdiff` library, used by millions of Node.js applications to compare text, contains a flaw in how it parses patch headers. By injecting obscure Unicode line separators (like `\u2028`) into a filename header, an attacker can cause the parser to enter an infinite loop. This leads to 100% CPU usage and eventual memory exhaustion, crashing the host process. Patched in versions 8.0.3, 5.2.2, and 4.0.4.

A critical Denial of Service vulnerability in the popular `jsdiff` library allows attackers to crash applications by supplying a patch file with specific Unicode line terminators. This triggers an infinite memory-leaking loop or a ReDOS condition.

The Hook: When a Dot Isn't a Dot

In the chaotic world of JavaScript ecosystem, jsdiff is a cornerstone. It's the engine under the hood of your favorite testing frameworks, code review tools, and git UI wrappers. It takes two strings and tells you what changed. Simple, right? But as we often find in security, the most dangerous vulnerabilities hide in the most mundane tasks—like parsing a file header.

CVE-2026-24001 is a classic reminder that relying on Regular Expressions for parsing structured data is like trying to perform surgery with a chainsaw: it might work, but it's going to be messy. The vulnerability exploits a fundamental quirk in JavaScript's Regex engine regarding what constitutes a "character."

The issue specifically targets the parsePatch method. When jsdiff consumes a unified diff (the kind with --- and +++ headers), it has to extract filenames. An attacker can supply a specially crafted string that looks valid enough to enter the parsing logic but invalid enough to break the internal state machine, causing the application to hang indefinitely. It’s a low-complexity, high-annoyance Denial of Service that turns your robust CI/CD pipeline into a paperweight.

The Flaw: The Infinite Loop of Death

To understand the bug, you have to understand the JavaScript Regular Expression dot (.). Most developers assume . matches "any character." But in JS (pre-flag s), the dot is actually a picky eater. It matches anything except line terminators. You know about \n and \r, but did you know about \u2028 (Line Separator) and \u2029 (Paragraph Separator)? Neither did the original regex author.

The vulnerable code uses a regex anchored with $ to match the filename in a patch header. The logic goes something like this:

  1. Look at the current line.
  2. Does it start with --- or +++?
  3. If yes, execute the regex /^(---|\+\+\+)\s+(.*)\r?$/ to extract the filename.
  4. If the regex matches, advance the parser index.

Here is the trap: The code checks if the line starts with --- to decide if it should parse a header. But then it uses the strict regex to actually do the parsing. If you provide a line like --- filename\u2028.js, the initial check says "Yes, this is a header!" But the extraction regex fails because the . stops at \u2028, and the $ anchor can't be reached at the end of the string. The regex returns null.

Because the match failed, the code assumes it didn't successfully consume the line. However, because the line still starts with ---, the loop comes back around, looks at the same line, and says "Hey, looks like a header!" It tries the regex again. It fails again. Infinite loop. Game over.

The Code: The Smoking Gun

Let's look at the vulnerable logic in src/patch/parse.ts. This is a textbook example of logic desynchronization between a condition and an operation.

The Vulnerable Code:

function parseFileHeader(index: Partial<StructuredPatch>) {
  // The loop logic (abstracted) keeps calling this if it sees --- or +++
  // But this regex is too strict:
  const fileHeader = (/^(---|\+\+\+)\s+(.*)\r?$/).exec(diffstr[i]);
  if (fileHeader) {
    // Logic to consume the line and increment 'i'
    // ...
  }
  // If fileHeader is null, 'i' is NEVER incremented,
  // but the parent loop doesn't know that and retries.
}

If exec returns null due to a hidden \u2028, the index i remains static. The fix involves abandoning the regex for the header extraction entirely. Instead of asking a regex to validate the whole line including the end, the developers switched to manual string slicing. This is far more robust because substring and split don't care about line separators.

The Fix (Commit 15a1585230748c8ae6f8274c202e0c87309142f5):

function parseFileHeader(index: Partial<StructuredPatch>) {
  // Relaxed check: just look for the start
  const fileHeaderMatch = (/^(---|\+\+\+)\s+/).exec(diffstr[i]);
  if (fileHeaderMatch) {
    const prefix = fileHeaderMatch[1];
    // Manual parsing > Regex parsing
    const data = diffstr[i].substring(3).trim().split('\t', 2);
    const header = (data[1] || '').trim();
    
    // Forcefully increment index regardless of what garbage follows
    i++; 
    // ...
  }
}

By mechanically splitting the string, the parser guarantees progress through the file, preventing the infinite loop state.

The Exploit: Weaponizing Unicode

Exploiting this is trivially easy if you can feed a patch file to a vulnerable system. Think of a Pull Request bot that parses diffs to check for style violations, or a web-based code viewer.

The Payload:

To trigger the crash, we just need a valid diff header structure with an invalid terminator inside the capture group.

// The poison pill
const payload = '--- a/malicious' + '\u2028' + 'file.js\n+++ b/malicious' + '\u2028' + 'file.js\n@@ -1,1 +1,1 @@\n-foo\n+bar';
 
const jsdiff = require('diff');
// This call will never return and will spike memory usage
jsdiff.parsePatch(payload);

When parsePatch hits that first line, the CPU spikes to 100%. Node.js's single-threaded event loop is blocked immediately. No other requests can be served. Eventually, as the loop (in some variations) accumulates data or just churns, the process may run out of memory or simply stay frozen until the orchestration layer (Kubernetes, etc.) kills it. If this is a serverless function, you're paying for the timeout duration every single time.

Attack Flow:

The Impact: Why Low CVSS is a Lie

The official CVSS score is a measly 2.7. This is misleading. The metrics likely penalized the score because it requires "User Interaction" (someone has to submit the patch) or because the "Availability" impact is listed as Low. Don't be fooled. In the context of a web service that automatically processes diffs (like a CI runner or a git platform), this is a High severity issue.

If you run a service where users supply code patches, this vulnerability allows any authenticated user (or unauthenticated, depending on the endpoint) to take down your parser service. It is an asymmetric attack: minimal effort for the attacker, maximum disruption for the server.

Furthermore, there is a secondary ReDOS (Regular Expression Denial of Service) vector identified in the preamble parsing. Even if the infinite loop doesn't catch you, the cubic complexity $O(n^3)$ of parsing massive headers with mixed terminators can cause CPU exhaustion just as effectively.

The Fix: Stop the Bleeding

The remediation is straightforward: upgrade. The maintainers have released patches for all major release lines.

[!NOTE] Action Required: Upgrade jsdiff immediately.

  • v6.x users: Upgrade to 6.0.0 or higher (specifically fixed in 8.0.3 per advisory).
  • v5.x users: Upgrade to 5.2.2.
  • v4.x users: Upgrade to 4.0.4.

If you cannot upgrade immediately (legacy codebases are fun, aren't they?), you must sanitize input before passing it to jsdiff. A simple string replacement can neutralize the exploit:

// Sanitize patch string before parsing
const safePatch = userPatch.replace(/[\u2028\u2029]/g, '');
jsdiff.parsePatch(safePatch);

This strips the dangerous separators, allowing the original regex to either match or fail cleanly without looping.

Fix Analysis (1)

Technical Appendix

CVSS Score
2.7/ 10
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

Affected Systems

Node.js applications using `jsdiff`Code review toolsGit web interfacesCI/CD pipelines processing patch filesTest frameworks using diff for assertions

Affected Versions Detail

Product
Affected Versions
Fixed Version
jsdiff
Kevin Decker
>= 6.0.0, < 8.0.38.0.3
jsdiff
Kevin Decker
>= 5.0.0, < 5.2.25.2.2
jsdiff
Kevin Decker
< 4.0.44.0.4
AttributeDetail
CWECWE-400 (Uncontrolled Resource Consumption)
Attack VectorNetwork
CVSS2.7 (Low)
Bug ClassInfinite Loop / Logic Error
Affected ComponentparsePatch function
Exploit ComplexityLow
CWE-400
Uncontrolled Resource Consumption

The software does not properly control the allocation and maintenance of a limited resource, allowing an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.

Vulnerability Timeline

Fix committed to main branch
2026-01-07
CVE-2026-24001 Published
2026-01-22

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.