CVE-2021-3803 (published 2021-09-17T07:15Z)

nth-check is vulnerable to Inefficient Regular Expression Complexity

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: CWE-1333
cwe_name: Inefficient Regular Expression Complexity
description: The product uses a regular expression with an inefficient, possibly exponential worst-case computational complexity that consumes excessive CPU cycles.
extended_description: Some regular expression engines have a feature called "backtracking". If the token cannot match, the engine "backtracks" to a position that may result in a different token that can match.
                Backtracking becomes a weakness if all of these conditions are met:
                
                    The number of possible backtracking attempts are exponential relative to the length of the input.
                    The input can fail to match the regular expression.
                    The input can be long enough.
                
		
		  Attackers can create crafted inputs that
		  intentionally cause the regular expression to use
		  excessive backtracking in a way that causes the CPU
		  consumption to spike.
		
            
            

Fix information:
repo_url: https://github.com/fb55/nth-check
commit_details: repo_url: https://github.com/fb55/nth-check
author: Felix Böhm
author_date: 2021-09-14 22:42:54+01:00
msg: fix(parse): Replace regex with hand-rolled parser (#9)
repository: repo_url: https://github.com/fb55/nth-check
repo_name: fb55/nth-check
description: Parses and compiles CSS nth-checks to highly optimized functions.
repo_language: TypeScript
owner: fb55
file_changes: filename: package-lock.json
change_type: ModificationType.MODIFY
diff: @@ -5,6 +5,7 @@
     "requires": true,
     "packages": {
         "": {
+            "name": "nth-check",
             "version": "2.0.0",
             "license": "BSD-2-Clause",
             "dependencies": {
@@ -726,7 +727,6 @@
                 "jest-resolve": "^26.6.2",
                 "jest-util": "^26.6.2",
                 "jest-worker": "^26.6.2",
-                "node-notifier": "^8.0.0",
                 "slash": "^3.0.0",
                 "source-map": "^0.6.0",
                 "string-length": "^4.0.1",
@@ -2228,8 +2228,7 @@
                 "esprima": "^4.0.1",
                 "estraverse": "^4.2.0",
                 "esutils": "^2.0.2",
-                "optionator": "^0.8.1",
-                "source-map": "~0.6.1"
+                "optionator": "^0.8.1"
             },
             "bin": {
                 "escodegen": "bin/escodegen.js",
@@ -3052,9 +3051,9 @@
             }
         },
         "node_modules/glob-parent": {
-            "version": "5.1.1",
-            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
-            "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
             "dev": true,
             "dependencies": {
                 "is-glob": "^4.0.1"
@@ -3228,9 +3227,9 @@
             }
         },
         "node_modules/hosted-git-info": {
-            "version": "2.8.8",
-            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
-            "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+            "version": "2.8.9",
+            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+            "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
             "dev": true
         },
         "node_modules/html-encoding-sniffer": {
@@ -3858,7 +3857,6 @@
                 "@types/node": "*",
                 "anymatch": "^3.0.3",
                 "fb-watchman": "^2.0.0",
-                "fsevents": "^2.1.2",
                 "graceful-fs": "^4.2.4",
                 "jest-regex-util": "^26.0.0",
                 "jest-serializer": "^26.6.2",
@@ -4415,9 +4413,9 @@
             }
         },
         "node_modules/lodash": {
-            "version": "4.17.20",
-            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-            "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
+            "version": "4.17.21",
+            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
             "dev": true
         },
         "node_modules/lodash.memoize": {
@@ -5000,9 +4998,9 @@
             }
         },
         "node_modules/path-parse": {
-            "version": "1.0.6",
-            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
-            "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+            "version": "1.0.7",
+            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+            "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
             "dev": true
         },
         "node_modules/path-type": {
@@ -7118,9 +7116,9 @@
             }
         },
         "node_modules/ws": {
-            "version": "7.4.0",
-            "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
-            "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
+            "version": "7.5.5",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
+            "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
             "dev": true,
             "engines": {
                 "node": ">=8.3.0"
@@ -9640,9 +9638,9 @@
             }
         },
         "glob-parent": {
-            "version": "5.1.1",
-            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
-            "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
             "dev": true,
             "requires": {
                 "is-glob": "^4.0.1"
@@ -9776,9 +9774,9 @@
             }
         },
         "hosted-git-info": {
-            "version": "2.8.8",
-            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
-            "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
+            "version": "2.8.9",
+            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+            "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
             "dev": true
         },
         "html-encoding-sniffer": {
@@ -10700,9 +10698,9 @@
             }
         },
         "lodash": {
-            "version": "4.17.20",
-            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-            "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
+            "version": "4.17.21",
+            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
             "dev": true
         },
         "lodash.memoize": {
@@ -11155,9 +11153,9 @@
             "dev": true
         },
         "path-parse": {
-            "version": "1.0.6",
-            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
-            "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+            "version": "1.0.7",
+            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+            "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
             "dev": true
         },
         "path-type": {
@@ -12828,9 +12826,9 @@
             }
         },
         "ws": {
-            "version": "7.4.0",
-            "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
-            "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
+            "version": "7.5.5",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
+            "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
             "dev": true,
             "requires": {}
         },

programming_language: JavaScript
filename: parse.ts
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,9 @@
 // Following http://www.w3.org/TR/css3-selectors/#nth-child-pseudo
 
-// [ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]?
-const RE_NTH_ELEMENT = /^([+-]?\d*n)?\s*(?:([+-]?)\s*(\d+))?$/;
+// Whitespace as per https://www.w3.org/TR/selectors-3/#lex is " \t\r\n\f"
+const whitespace = new Set([9, 10, 12, 13, 32]);
+const ZERO = "0".charCodeAt(0);
+const NINE = "9".charCodeAt(0);
 
 /**
  * Parses an expression.
@@ -19,24 +21,72 @@ export function parse(formula: string): [a: number, b: number] {
         return [2, 1];
     }
 
-    const parsed = formula.match(RE_NTH_ELEMENT);
+    // Parse [ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]?
 
-    if (!parsed) {
+    let idx = 0;
+
+    let a = 0;
+    let sign = readSign();
+    let number = readNumber();
+
+    if (idx < formula.length && formula.charAt(idx) === "n") {
+        idx++;
+        a = sign * (number ?? 1);
+
+        skipWhitespace();
+
+        if (idx < formula.length) {
+            sign = readSign();
+            skipWhitespace();
+            number = readNumber();
+        } else {
+            sign = number = 0;
+        }
+    }
+
+    // Throw if there is anything else
+    if (number === null || idx < formula.length) {
         throw new Error(`n-th rule couldn't be parsed ('${formula}')`);
     }
 
-    let a;
+    return [a, sign * number];
 
-    if (parsed[1]) {
-        a = parseInt(parsed[1], 10);
-        if (isNaN(a)) {
-            a = parsed[1].startsWith("-") ? -1 : 1;
+    function readSign() {
+        if (formula.charAt(idx) === "-") {
+            idx++;
+            return -1;
         }
-    } else a = 0;
 
-    const b =
-        (parsed[2] === "-" ? -1 : 1) *
-        (parsed[3] ? parseInt(parsed[3], 10) : 0);
+        if (formula.charAt(idx) === "+") {
+            idx++;
+        }
 
-    return [a, b];
+        return 1;
+    }
+
+    function readNumber() {
+        const start = idx;
+        let value = 0;
+
+        while (
+            idx < formula.length &&
+            formula.charCodeAt(idx) >= ZERO &&
+            formula.charCodeAt(idx) <= NINE
+        ) {
+            value = value * 10 + (formula.charCodeAt(idx) - ZERO);
+            idx++;
+        }
+
+        // Return `null` if we didn't read anything.
+        return idx === start ? null : value;
+    }
+
+    function skipWhitespace() {
+        while (
+            idx < formula.length &&
+            whitespace.has(formula.charCodeAt(idx))
+        ) {
+            idx++;
+        }
+    }
 }

programming_language: TypeScript