CVE-2021-23371 (published 2021-04-12T13:15Z)

This affects the package chrono-node before 2.2.4. It hangs on a date-like string with lots of embedded spaces.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:N/A:P)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/wanasit/chrono
commit_details: repo_url: https://github.com/wanasit/chrono
author: Wanasit Tanakitrungruang
author_date: 2021-03-13 15:30:07+09:00
msg: Fix: unblounded regex backtracking in timeunit parsing
repository: repo_url: https://github.com/wanasit/chrono
repo_name: wanasit/chrono
description: A natural language date parser in Javascript
repo_language: TypeScript
owner: wanasit
file_changes: filename: constants.ts
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,5 @@
 import { OpUnitType } from "dayjs";
-import { matchAnyPattern } from "../../utils/pattern";
+import { matchAnyPattern, repeatedTimeunitPattern } from "../../utils/pattern";
 import { findMostLikelyADYear } from "../../calculation/years";
 import { TimeUnits } from "../../utils/timeunits";
 
@@ -149,14 +149,10 @@ export function parseYear(match: string): number {
 
 //-----------------------------
 
-const SINGLE_TIME_UNIT_PATTERN = `(${NUMBER_PATTERN})\\s*(${matchAnyPattern(TIME_UNIT_DICTIONARY)})\\s*`;
+const SINGLE_TIME_UNIT_PATTERN = `(${NUMBER_PATTERN})\\s{0,5}(${matchAnyPattern(TIME_UNIT_DICTIONARY)})\\s{0,5}`;
 const SINGLE_TIME_UNIT_REGEX = new RegExp(SINGLE_TIME_UNIT_PATTERN, "i");
 
-const SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE = SINGLE_TIME_UNIT_PATTERN.replace(/\((?!\?)/g, "(?:");
-
-export const TIME_UNITS_PATTERN =
-    `(?:(?:about|around)\\s*)?` +
-    `${SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE}\\s*(?:,?\\s*${SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE})*`;
+export const TIME_UNITS_PATTERN = repeatedTimeunitPattern("", SINGLE_TIME_UNIT_PATTERN);
 
 export function parseTimeUnits(timeunitText): TimeUnits {
     const fragments = {};

programming_language: TypeScript
filename: constants.ts
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,5 @@
 import { OpUnitType } from "dayjs";
-import { matchAnyPattern } from "../../utils/pattern";
+import { matchAnyPattern, repeatedTimeunitPattern } from "../../utils/pattern";
 import { findMostLikelyADYear } from "../../calculation/years";
 import { TimeUnits } from "../../utils/timeunits";
 
@@ -223,14 +223,10 @@ export function parseYear(match: string): number {
 
 //-----------------------------
 
-const SINGLE_TIME_UNIT_PATTERN = `(${NUMBER_PATTERN})\\s*(${matchAnyPattern(TIME_UNIT_DICTIONARY)})\\s*`;
+const SINGLE_TIME_UNIT_PATTERN = `(${NUMBER_PATTERN})\\s{0,5}(${matchAnyPattern(TIME_UNIT_DICTIONARY)})\\s{0,5}`;
 const SINGLE_TIME_UNIT_REGEX = new RegExp(SINGLE_TIME_UNIT_PATTERN, "i");
 
-const SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE = SINGLE_TIME_UNIT_PATTERN.replace(/\((?!\?)/g, "(?:");
-
-export const TIME_UNITS_PATTERN =
-    `(?:(?:about|around)\\s*)?` +
-    `${SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE}\\s*(?:,?\\s*${SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE})*`;
+export const TIME_UNITS_PATTERN = repeatedTimeunitPattern(`(?:(?:about|around)\\s*)?`, SINGLE_TIME_UNIT_PATTERN);
 
 export function parseTimeUnits(timeunitText): TimeUnits {
     const fragments = {};

programming_language: TypeScript
filename: constants.ts
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,5 @@
 import { OpUnitType, QUnitType } from "dayjs";
-import { matchAnyPattern } from "../../utils/pattern";
+import { matchAnyPattern, repeatedTimeunitPattern } from "../../utils/pattern";
 
 export const WEEKDAY_DICTIONARY: { [word: string]: number } = {
     "dimanche": 0,
@@ -160,12 +160,10 @@ export function parseYear(match: string): number {
 
 //-----------------------------
 
-const SINGLE_TIME_UNIT_PATTERN = `(${NUMBER_PATTERN})\\s*(${matchAnyPattern(TIME_UNIT_DICTIONARY)})\\s*`;
+const SINGLE_TIME_UNIT_PATTERN = `(${NUMBER_PATTERN})\\s{0,5}(${matchAnyPattern(TIME_UNIT_DICTIONARY)})\\s{0,5}`;
 const SINGLE_TIME_UNIT_REGEX = new RegExp(SINGLE_TIME_UNIT_PATTERN, "i");
 
-const SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE = SINGLE_TIME_UNIT_PATTERN.replace(/\((?!\?)/g, "(?:");
-
-export const TIME_UNITS_PATTERN = `(?:${SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE})+`;
+export const TIME_UNITS_PATTERN = repeatedTimeunitPattern("", SINGLE_TIME_UNIT_PATTERN);
 
 export function parseTimeUnits(timeunitText): { [c in OpUnitType | QUnitType]?: number } {
     const fragments = {};

programming_language: TypeScript
filename: constants.ts
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,5 @@
 import { OpUnitType } from "dayjs";
-import { matchAnyPattern } from "../../utils/pattern";
+import { matchAnyPattern, repeatedTimeunitPattern } from "../../utils/pattern";
 import { findMostLikelyADYear } from "../../calculation/years";
 import { TimeUnits } from "../../utils/timeunits";
 
@@ -207,14 +207,10 @@ export function parseYear(match: string): number {
 
 //-----------------------------
 
-const SINGLE_TIME_UNIT_PATTERN = `(${NUMBER_PATTERN})\\s*(${matchAnyPattern(TIME_UNIT_DICTIONARY)})\\s*`;
+const SINGLE_TIME_UNIT_PATTERN = `(${NUMBER_PATTERN})\\s{0,5}(${matchAnyPattern(TIME_UNIT_DICTIONARY)})\\s{0,5}`;
 const SINGLE_TIME_UNIT_REGEX = new RegExp(SINGLE_TIME_UNIT_PATTERN, "i");
 
-const SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE = SINGLE_TIME_UNIT_PATTERN.replace(/\((?!\?)/g, "(?:");
-
-export const TIME_UNITS_PATTERN =
-    `(?:(?:binnen|in)\\s*)?` +
-    `${SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE}\\s*(?:,?\\s*${SINGLE_TIME_UNIT_PATTERN_NO_CAPTURE})*`;
+export const TIME_UNITS_PATTERN = repeatedTimeunitPattern(`(?:(?:binnen|in)\\s*)?`, SINGLE_TIME_UNIT_PATTERN);
 
 export function parseTimeUnits(timeunitText): TimeUnits {
     const fragments = {};

programming_language: TypeScript
filename: pattern.ts
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,8 @@
 type DictionaryLike = string[] | { [word: string]: unknown } | Map<string, unknown>;
 
-export function patternWithWordBreak(regExp: RegExp): RegExp {
-    return RegExp("" + regExp.source);
+export function repeatedTimeunitPattern(prefix: string, singleTimeunitPattern: string): string {
+    const singleTimeunitPatternNoCapture = singleTimeunitPattern.replace(/\((?!\?)/g, "(?:");
+    return `${prefix}${singleTimeunitPatternNoCapture}\\s*(?:,?\\s{0,5}${singleTimeunitPatternNoCapture}){0,10}`;
 }
 
 export function extractTerms(dictionary: DictionaryLike): string[] {

programming_language: TypeScript
filename: en_performance.test.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,27 @@
+import * as chrono from "../../src";
+import { measureMilliSec } from "../test_util";
+
+test("Test - Benchmarking against whitespace backtracking", () => {
+    const time = measureMilliSec(() => {
+        const str =
+            "BGR3                                                                                         " +
+            "                                                                                        186          " +
+            "                                      days                                                           " +
+            "                                                                                                     " +
+            "                                                                                                     " +
+            "           18                                                hours                                   " +
+            "                                                                                                     " +
+            "                                                                                                     " +
+            "                                   37                                                minutes         " +
+            "                                                                                                     " +
+            "                                                                                                     " +
+            "                                                             01                                      " +
+            "          seconds";
+
+        const results = chrono.parse(str);
+        expect(results.length).toBe(0);
+    });
+
+    console.log(time);
+    expect(time).toBeLessThan(1000);
+});

programming_language: TypeScript
filename: test_util.ts
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,5 @@
 import { BufferedDebugHandler } from "../src/debugging";
-import { ParsedResult, ParsingOption } from "../src";
+import { en, ParsedResult, ParsingOption } from "../src";
 
 interface ChronoLike {
     parse(text: string, ref?: Date, option?: ParsingOption): ParsedResult[];
@@ -73,6 +73,13 @@ export function testUnexpectedResult(chrono: ChronoLike, text: string, refDate?:
     }
 }
 
+export function measureMilliSec(block: () => void): number {
+    const startTime = new Date().getMilliseconds();
+    block();
+    const endTime = new Date().getMilliseconds();
+    return endTime - startTime;
+}
+
 // --------------------------------------------------
 
 declare global {

programming_language: TypeScript