CVE-2020-26256: Uncontrolled Resource Consumption (CWE-400)
value: Fast-csv is an npm package for parsing and formatting CSVs or any other delimited value file in node. In fast-cvs before version 4.3.6 there is a possible ReDoS vulnerability (Regular Expression Denial of Service) when using ignoreEmpty option when parsing. This has been patched in `v4.3.6` You will only be affected by this if you use the `ignoreEmpty` parsing option. If you do use this option it is recommended that you upgrade to the latest version `v4.3.6` This vulnerability was found using a CodeQL query which identified `EMPTY_ROW_REGEXP` regular expression as vulnerable.
CWE description: The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
CVSS v3 base score: 6.5
Published: 2020-12-08T22:15Z
Repository: https://github.com/C2FO/fast-csv
Language: TypeScript
Files changed: packages/parse/__tests__/issues/issue540.spec.ts, packages/parse/src/parser/Parser.ts, packages/parse/src/parser/RowParser.ts
Commit date: 2020-12-03 22:41:52 -0700
Commit message: fix: Simplify empty row check by removing complex regex

Diff: diff --git a/packages/parse/__tests__/issues/issue540.spec.ts b/packages/parse/__tests__/issues/issue540.spec.ts
new file mode 100644
index 0000000..25cce91
--- /dev/null
+++ b/packages/parse/__tests__/issues/issue540.spec.ts
@@ -0,0 +1,33 @@
+import { EOL } from 'os';
+import { parseString, RowMap, RowArray } from '../../src';
+
+describe('Issue #540 - https://github.com/C2FO/fast-csv/issues/540', () => {
+    const CSV_CONTENT = [
+        ' , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , -',
+    ].join(EOL);
+
+    const expectedRows = [
+        [
+            ' , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,' +
+                ' , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,' +
+                ' , , , , , , , , , , , , , , -',
+        ],
+    ];
+
+    it('allow transforming to any object shape', () => {
+        return new Promise((res, rej) => {
+            const invalid: RowArray[] = [];
+            const rows: RowMap[] = [];
+            parseString(CSV_CONTENT, { ignoreEmpty: true, delimiter: '\t' })
+                .on('data-invalid', (row: RowArray) => invalid.push(row))
+                .on('data', (r) => rows.push(r))
+                .on('error', rej)
+                .on('end', (count: number) => {
+                    expect(rows).toEqual(expectedRows);
+                    expect(invalid).toHaveLength(0);
+                    expect(count).toBe(expectedRows.length + invalid.length);
+                    res();
+                });
+        });
+    });
+});
diff --git a/packages/parse/src/parser/Parser.ts b/packages/parse/src/parser/Parser.ts
index 6e4335d..03f0b26 100644
--- a/packages/parse/src/parser/Parser.ts
+++ b/packages/parse/src/parser/Parser.ts
@@ -2,12 +2,10 @@ import { Scanner } from './Scanner';
 import { RowParser } from './RowParser';
 import { ParserOptions } from '../ParserOptions';
 import { RowArray } from '../types';
 import { Token } from './Token';
 
-const EMPTY_ROW_REGEXP = /^\s*(?:''|"")?\s*(?:,\s*(?:''|"")?\s*)*$/;
-
 export interface ParseResult {
     line: string;
     rows: string[][];
 }
 export class Parser {
@@ -77,11 +75,11 @@ export class Parser {
         }
         const row = this.rowParser.parse(scanner);
         if (row === null) {
             return false;
         }
-        if (this.parserOptions.ignoreEmpty && EMPTY_ROW_REGEXP.test(row.join(''))) {
+        if (this.parserOptions.ignoreEmpty && RowParser.isEmptyRow(row)) {
             return true;
         }
         rows.push(row);
         return true;
     }
diff --git a/packages/parse/src/parser/RowParser.ts b/packages/parse/src/parser/RowParser.ts
index b37f27e..18af6fa 100644
--- a/packages/parse/src/parser/RowParser.ts
+++ b/packages/parse/src/parser/RowParser.ts
@@ -2,11 +2,17 @@ import { Scanner } from './Scanner';
 import { ColumnParser } from './column';
 import { ParserOptions } from '../ParserOptions';
 import { RowArray } from '../types';
 import { MaybeToken, Token } from './Token';
 
+const EMPTY_STRING = '';
+
 export class RowParser {
+    static isEmptyRow(row: RowArray): boolean {
+        return row.join(EMPTY_STRING).replace(/\s+/g, EMPTY_STRING) === EMPTY_STRING;
+    }
+
     private readonly parserOptions: ParserOptions;
 
     private readonly columnParser: ColumnParser;
 
     public constructor(parserOptions: ParserOptions) {

Vulnerable code: const EMPTY_ROW_REGEXP = /^\s*(?:''|"")?\s*(?:,\s*(?:''|"")?\s*)*$/;

        if (this.parserOptions.ignoreEmpty && EMPTY_ROW_REGEXP.test(row.join(''))) {
Fixed code: import { EOL } from 'os';
import { parseString, RowMap, RowArray } from '../../src';

describe('Issue #540 - https://github.com/C2FO/fast-csv/issues/540', () => {
    const CSV_CONTENT = [
        ' , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , -',
    ].join(EOL);

    const expectedRows = [
        [
            ' , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,' +
                ' , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,' +
                ' , , , , , , , , , , , , , , -',
        ],
    ];

    it('allow transforming to any object shape', () => {
        return new Promise((res, rej) => {
            const invalid: RowArray[] = [];
            const rows: RowMap[] = [];
            parseString(CSV_CONTENT, { ignoreEmpty: true, delimiter: '\t' })
                .on('data-invalid', (row: RowArray) => invalid.push(row))
                .on('data', (r) => rows.push(r))
                .on('error', rej)
                .on('end', (count: number) => {
                    expect(rows).toEqual(expectedRows);
                    expect(invalid).toHaveLength(0);
                    expect(count).toBe(expectedRows.length + invalid.length);
                    res();
                });
        });
    });
});
        if (this.parserOptions.ignoreEmpty && RowParser.isEmptyRow(row)) {
const EMPTY_STRING = '';

    static isEmptyRow(row: RowArray): boolean {
        return row.join(EMPTY_STRING).replace(/\s+/g, EMPTY_STRING) === EMPTY_STRING;
    }