CVE-2024-4068: Insufficient Information (NVD-CWE-noinfo)
value: The NPM package `braces`, versions prior to 3.0.3, fails to limit the number of characters it can handle, which could lead to Memory Exhaustion. In `lib/parse.js,` if a malicious user sends "imbalanced braces" as input, the parsing will enter a loop, which will cause the program to start allocating heap memory without freeing it at any moment of the loop. Eventually, the JavaScript heap limit is reached, and the program will crash.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-14T15:42Z
Repository: https://github.com/micromatch/braces
Language: JavaScript
Files changed: .verb.md, README.md, lib/constants.js, lib/parse.js, test/braces.parse.js
Commit date: 2024-05-21 03:42:30 -0400
Commit message: Snyk js braces 6838727 (#40)

* Remove maxSymbols from README

* Revert "Merge pull request #37 from coderaiser/fix/vulnerability"

This reverts commit a5851e57f45c3431a94d83fc565754bc10f5bbc3, reversing
changes made to 98414f9f1fabe021736e26836d8306d5de747e0d.

* Lower defaultLength to 10000
Diff: diff --git a/.verb.md b/.verb.md
index cdedd55..2ee6767 100644
--- a/.verb.md
+++ b/.verb.md
@@ -165,11 +165,11 @@ console.log(braces.expand('a{b}c'));
 
 ### options.maxLength
 
 **Type**: `Number`
 
-**Default**: `65,536`
+**Default**: `10,000`
 
 **Description**: Limit the length of the input string. Useful when the input string is generated or your application allows users to pass a string, et cetera.
 
 ```js
 console.log(braces('a/{b,c}/d', { maxLength: 3 }));  //=> throws an error
diff --git a/README.md b/README.md
index 9d9f9bc..f59dd60 100644
--- a/README.md
+++ b/README.md
@@ -176,30 +176,18 @@ console.log(braces.expand('a{b}c'));
 
 ### options.maxLength
 
 **Type**: `Number`
 
-**Default**: `65,536`
+**Default**: `10,000`
 
 **Description**: Limit the length of the input string. Useful when the input string is generated or your application allows users to pass a string, et cetera.
 
 ```js
 console.log(braces('a/{b,c}/d', { maxLength: 3 })); //=> throws an error
 ```
 
-### options.maxSymbols
-
-**Type**: `Number`
-
-**Default**: `1024`
-
-**Description**: Limit the count of unique symbols the input string.
-
-```js
-console.log(braces('a/{b,c}/d', { maxSymbols: 2 })); //=> throws an error
-```
-
 ### options.expand
 
 **Type**: `Boolean`
 
 **Default**: `undefined`
diff --git a/lib/constants.js b/lib/constants.js
index 3280bea..2bb3b88 100644
--- a/lib/constants.js
+++ b/lib/constants.js
@@ -1,10 +1,9 @@
 'use strict';
 
 module.exports = {
-  MAX_LENGTH: 1024 * 64,
-  MAX_SYMBOLS: 1024,
+  MAX_LENGTH: 10000,
 
   // Digits
   CHAR_0: '0', /* 0 */
   CHAR_9: '9', /* 9 */
 
diff --git a/lib/parse.js b/lib/parse.js
index 7a724b8..145ea26 100644
--- a/lib/parse.js
+++ b/lib/parse.js
@@ -1,17 +1,15 @@
 'use strict';
 
 const stringify = require('./stringify');
-const {isCorrectBraces, validateInput} = require('./validate-input');
 
 /**
  * Constants
  */
 
 const {
   MAX_LENGTH,
-  MAX_SYMBOLS,
   CHAR_BACKSLASH, /* \ */
   CHAR_BACKTICK, /* ` */
   CHAR_COMMA, /* , */
   CHAR_DOT, /* . */
   CHAR_LEFT_PARENTHESES, /* ( */
@@ -34,15 +32,10 @@ const parse = (input, options = {}) => {
   if (typeof input !== 'string') {
     throw new TypeError('Expected a string');
   }
 
   let opts = options || {};
-
-  validateInput(input, {
-    maxSymbols: opts.maxSymbols || MAX_SYMBOLS,
-  });
-
   let max = typeof opts.maxLength === 'number' ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH;
   if (input.length > max) {
     throw new SyntaxError(`Input length (${input.length}), exceeds max characters (${max})`);
   }
 
@@ -309,45 +302,32 @@ const parse = (input, options = {}) => {
      */
 
     push({ type: 'text', value });
   }
 
-  flattenBlocks(stack)
-  markImbalancedBraces(ast);
-  push({ type: 'eos' });
-
-  return ast;
-};
-
-module.exports = parse;
-
-function markImbalancedBraces({nodes}) {
   // Mark imbalanced braces and brackets as invalid
-  for (const node of nodes) {
-    if (!node.nodes && !node.invalid) {
-      if (node.type === 'open') node.isOpen = true;
-      if (node.type === 'close') node.isClose = true;
-      if (!node.nodes) node.type = 'text';
-
-      node.invalid = true;
-    }
-
-    delete node.parent;
-    delete node.prev;
-  }
-}
-
-function flattenBlocks(stack) {
-  let block;
   do {
     block = stack.pop();
 
-    if (block.type === 'root')
-      continue;
+    if (block.type !== 'root') {
+      block.nodes.forEach(node => {
+        if (!node.nodes) {
+          if (node.type === 'open') node.isOpen = true;
+          if (node.type === 'close') node.isClose = true;
+          if (!node.nodes) node.type = 'text';
+          node.invalid = true;
+        }
+      });
 
-    // get the location of the block on parent.nodes (block's siblings)
-    let parent = stack.at(-1);
-    let index = parent.nodes.indexOf(block);
-    // replace the (invalid) block with its nodes
-    parent.nodes.splice(index, 1, ...block.nodes);
+      // get the location of the block on parent.nodes (block's siblings)
+      let parent = stack[stack.length - 1];
+      let index = parent.nodes.indexOf(block);
+      // replace the (invalid) block with it's nodes
+      parent.nodes.splice(index, 1, ...block.nodes);
+    }
   } while (stack.length > 0);
-}
+
+  push({ type: 'eos' });
+  return ast;
+};
+
+module.exports = parse;
diff --git a/lib/validate-input.js b/lib/validate-input.js
deleted file mode 100644
index 0f987b6..0000000
--- a/lib/validate-input.js
+++ /dev/null
@@ -1,12 +0,0 @@
-module.exports.validateInput = (line, {maxSymbols}) => {
-  const symbols = {};
-
-    for (const current of line) {
-        symbols[current] = (symbols[current] || 0) + 1;
-    }
-
-    for (const [value, count] of Object.entries(symbols)) {
-      if (count > maxSymbols)
-        throw SyntaxError(`To many symbols '${value}'. Maximum: ${maxSymbols} allowed. Received: ${count}`);
-    }
-};
diff --git a/test/braces.parse.js b/test/braces.parse.js
index 09a2361..b814558 100644
--- a/test/braces.parse.js
+++ b/test/braces.parse.js
@@ -8,20 +8,10 @@ describe('braces.parse()', () => {
   describe('errors', () => {
     it('should throw an error when string exceeds max safe length', () => {
       let MAX_LENGTH = 1024 * 64;
       assert.throws(() => parse('.'.repeat(MAX_LENGTH + 2)));
     });
-    it('should throw an error when symbols exceeds max symbols count default', () => {
-      let SYMBOLS= 1024;
-      assert.throws(() => parse('.'.repeat(MAX_SYMBOLS * 2)));
-    });
-    it('should throw an error when symbols exceeds max symbols count ', () => {
-      let SYMBOLS= 2;
-      assert.throws(() => parse('...', {
-        maxSymbols: 2,
-      }));
-    });
   });
 
   describe('valid', () => {
     it('should return an AST', () => {
       let ast = parse('a/{b,c}/d');

Vulnerable code: **Default**: `65,536`
**Default**: `65,536`
### options.maxSymbols

**Type**: `Number`

**Default**: `1024`

**Description**: Limit the count of unique symbols the input string.

```js
console.log(braces('a/{b,c}/d', { maxSymbols: 2 })); //=> throws an error
```

  MAX_LENGTH: 1024 * 64,
  MAX_SYMBOLS: 1024,
const {isCorrectBraces, validateInput} = require('./validate-input');
  MAX_SYMBOLS,

  validateInput(input, {
    maxSymbols: opts.maxSymbols || MAX_SYMBOLS,
  });

  flattenBlocks(stack)
  markImbalancedBraces(ast);
  push({ type: 'eos' });

  return ast;
};

module.exports = parse;

function markImbalancedBraces({nodes}) {
  for (const node of nodes) {
    if (!node.nodes && !node.invalid) {
      if (node.type === 'open') node.isOpen = true;
      if (node.type === 'close') node.isClose = true;
      if (!node.nodes) node.type = 'text';

      node.invalid = true;
    }

    delete node.parent;
    delete node.prev;
  }
}

function flattenBlocks(stack) {
  let block;
    if (block.type === 'root')
      continue;
    // get the location of the block on parent.nodes (block's siblings)
    let parent = stack.at(-1);
    let index = parent.nodes.indexOf(block);
    // replace the (invalid) block with its nodes
    parent.nodes.splice(index, 1, ...block.nodes);
}
module.exports.validateInput = (line, {maxSymbols}) => {
  const symbols = {};

    for (const current of line) {
        symbols[current] = (symbols[current] || 0) + 1;
    }

    for (const [value, count] of Object.entries(symbols)) {
      if (count > maxSymbols)
        throw SyntaxError(`To many symbols '${value}'. Maximum: ${maxSymbols} allowed. Received: ${count}`);
    }
};
    it('should throw an error when symbols exceeds max symbols count default', () => {
      let SYMBOLS= 1024;
      assert.throws(() => parse('.'.repeat(MAX_SYMBOLS * 2)));
    });
    it('should throw an error when symbols exceeds max symbols count ', () => {
      let SYMBOLS= 2;
      assert.throws(() => parse('...', {
        maxSymbols: 2,
      }));
    });
Fixed code: **Default**: `10,000`
**Default**: `10,000`
  MAX_LENGTH: 10000,
    if (block.type !== 'root') {
      block.nodes.forEach(node => {
        if (!node.nodes) {
          if (node.type === 'open') node.isOpen = true;
          if (node.type === 'close') node.isClose = true;
          if (!node.nodes) node.type = 'text';
          node.invalid = true;
        }
      });
      // get the location of the block on parent.nodes (block's siblings)
      let parent = stack[stack.length - 1];
      let index = parent.nodes.indexOf(block);
      // replace the (invalid) block with it's nodes
      parent.nodes.splice(index, 1, ...block.nodes);
    }

  push({ type: 'eos' });
  return ast;
};

module.exports = parse;