diff --git a/javascript/ql/lib/semmle/javascript/security/performance/ReDoSUtil.qll b/javascript/ql/lib/semmle/javascript/security/performance/ReDoSUtil.qll
index cec3b654acd..7730821734f 100644
--- a/javascript/ql/lib/semmle/javascript/security/performance/ReDoSUtil.qll
+++ b/javascript/ql/lib/semmle/javascript/security/performance/ReDoSUtil.qll
@@ -98,6 +98,8 @@ predicate matchesEpsilon(RegExpTerm t) {
matchesEpsilon(t.(RegExpBackRef).getGroup())
or
forex(RegExpTerm child | child = t.(RegExpSequence).getAChild() | matchesEpsilon(child))
+ or
+ t.(RegExpRange).getLowerBound() = 0
}
/**
@@ -539,6 +541,49 @@ private class EdgeLabel extends TInputSymbol {
}
}
+/**
+ * A RegExp term that acts like a plus.
+ * Either it's a RegExpPlus, or it is a range {1,X} where X is >= 30.
+ * 30 has been chosen as a threshold because for exponential blowup 2^30 is enough to get a decent DOS attack.
+ */
+private class EffectivelyPlus extends RegExpTerm {
+ EffectivelyPlus() {
+ this instanceof RegExpPlus
+ or
+ exists(RegExpRange range | range.getLowerBound() = 1 and range.getUpperBound() >= 30 |
+ this = range
+ )
+ }
+}
+
+/**
+ * A RegExp term that acts like a star.
+ * Either it's a RegExpStar, or it is a range {0,X} where X is >= 30.
+ */
+private class EffectivelyStar extends RegExpTerm {
+ EffectivelyStar() {
+ this instanceof RegExpStar
+ or
+ exists(RegExpRange range | range.getLowerBound() = 0 and range.getUpperBound() >= 30 |
+ this = range
+ )
+ }
+}
+
+/**
+ * A RegExp term that acts like a question mark.
+ * Either it's a RegExpQuestion, or it is a range {0,1}.
+ */
+private class EffectivelyQuestion extends RegExpTerm {
+ EffectivelyQuestion() {
+ this instanceof RegExpOpt
+ or
+ exists(RegExpRange range | range.getLowerBound() = 0 and range.getUpperBound() = 1 |
+ this = range
+ )
+ }
+}
+
/**
* Gets the state before matching `t`.
*/
@@ -559,14 +604,14 @@ State after(RegExpTerm t) {
or
exists(RegExpGroup grp | t = grp.getAChild() | result = after(grp))
or
- exists(RegExpStar star | t = star.getAChild() | result = before(star))
+ exists(EffectivelyStar star | t = star.getAChild() | result = before(star))
or
- exists(RegExpPlus plus | t = plus.getAChild() |
+ exists(EffectivelyPlus plus | t = plus.getAChild() |
result = before(plus) or
result = after(plus)
)
or
- exists(RegExpOpt opt | t = opt.getAChild() | result = after(opt))
+ exists(EffectivelyQuestion opt | t = opt.getAChild() | result = after(opt))
or
exists(RegExpRoot root | t = root | result = AcceptAnySuffix(root))
}
@@ -617,15 +662,17 @@ predicate delta(State q1, EdgeLabel lbl, State q2) {
or
exists(RegExpGroup grp | lbl = Epsilon() | q1 = before(grp) and q2 = before(grp.getChild(0)))
or
- exists(RegExpStar star | lbl = Epsilon() |
+ exists(EffectivelyStar star | lbl = Epsilon() |
q1 = before(star) and q2 = before(star.getChild(0))
or
q1 = before(star) and q2 = after(star)
)
or
- exists(RegExpPlus plus | lbl = Epsilon() | q1 = before(plus) and q2 = before(plus.getChild(0)))
+ exists(EffectivelyPlus plus | lbl = Epsilon() |
+ q1 = before(plus) and q2 = before(plus.getChild(0))
+ )
or
- exists(RegExpOpt opt | lbl = Epsilon() |
+ exists(EffectivelyQuestion opt | lbl = Epsilon() |
q1 = before(opt) and q2 = before(opt.getChild(0))
or
q1 = before(opt) and q2 = after(opt)
diff --git a/javascript/ql/test/query-tests/Performance/ReDoS/PolynomialBackTracking.expected b/javascript/ql/test/query-tests/Performance/ReDoS/PolynomialBackTracking.expected
index fc71a84b378..aa34a13c4e1 100644
--- a/javascript/ql/test/query-tests/Performance/ReDoS/PolynomialBackTracking.expected
+++ b/javascript/ql/test/query-tests/Performance/ReDoS/PolynomialBackTracking.expected
@@ -201,6 +201,7 @@
| regexplib/markup.js:13:14:13:16 | .+? | Strings starting with '<' and with many repetitions of '!' can start matching anywhere after the start of the preceeding .*? |
| regexplib/markup.js:14:13:14:14 | .* | Strings starting with '<' and with many repetitions of 'a' can start matching anywhere after the start of the preceeding .* |
| regexplib/markup.js:14:24:14:25 | .* | Strings starting with '<>' and with many repetitions of '>a' can start matching anywhere after the start of the preceeding .* |
+| regexplib/markup.js:15:16:15:18 | .*? | Strings starting with '
|
| regexplib/markup.js:16:5:16:9 | [^>]* | Strings starting with 'src' and with many repetitions of 'src' can start matching anywhere after the start of the preceeding src[^>]*[^/].(?:jpg\|bmp\|gif)(?:\\"\|\\') |
| regexplib/markup.js:17:8:17:24 | (\\s(\\w*=".*?")?)* | Strings starting with ']+color.*>) #IF\\/THEN lookahead color in tag (.*?color\\s*?[=\|:]\\s*?) # IF found THEN move ahead ('+\\#*?[\\w\\s]*'+ # CAPTURE ColorName\\/Hex \|"+\\#*?[\\w\\s]*"+ # single or double \|\\#*\\w*\\b) # or no quotes\t.*?> # & move to end of tag \|.*?> # ELSE move to end of Tag ) # Close the If\\/Then lookahead # Use Multiline and IgnoreCase # Replace the matches from RE with MatchEvaluator below: # if m.Groups(1).Value<>"" then # Return "" # else # Return "" # end if |
+| regexplib/markup.js:24:39:24:41 | \\s+ | Strings starting with '<A' and with many repetitions of ' - != ' can start matching anywhere after the start of the preceeding \\s* |
+| regexplib/markup.js:24:43:24:45 | \\S+ | Strings starting with '<A ' and with many repetitions of '- !=' can start matching anywhere after the start of the preceeding \\s* |
+| regexplib/markup.js:24:48:24:50 | \\s* | Strings starting with '<A !' and with many repetitions of ' =- ! ' can start matching anywhere after the start of the preceeding \\s+ |
+| regexplib/markup.js:24:52:24:54 | \\s* | Strings starting with '<A !=' and with many repetitions of '- !=' can start matching anywhere after the start of the preceeding \\S+ |
| regexplib/markup.js:25:11:25:15 | [^>]* | Strings starting with ']*\\son\\w+=(\\w+\|'[^']*'\|"[^"]*")[^>]*> |
| regexplib/markup.js:25:45:25:49 | [^>]* | Strings starting with ']* | Strings starting with '<' and with many repetitions of '<' can start matching anywhere after the start of the preceeding <[^>]*name[\\s]*=[\\s]*"?[^\\w_]*"?[^>]*> |
@@ -228,6 +233,10 @@
| regexplib/markup.js:44:3:44:7 | [^>]* | Strings starting with '<' and with many repetitions of '<' can start matching anywhere after the start of the preceeding <[^>]*name[\\s]*=[\\s]*"?[^\\w_]*"?[^>]*> |
| regexplib/markup.js:44:34:44:38 | [^>]* | Strings starting with ' |
| regexplib/markup.js:53:15:53:19 | [\\w]* | Strings starting with '[a' and with many repetitions of '0' can start matching anywhere after the start of the preceeding \\w+ |
| regexplib/markup.js:56:23:56:25 | \\w+ | Strings with many repetitions of 'a' can start matching anywhere after the start of the preceeding (\\/?(?\\w+))+ |
@@ -300,6 +309,7 @@
| regexplib/strings.js:14:61:14:63 | \\w* | Strings starting with 'AA' and with many repetitions of 'A' can start matching anywhere after the start of the preceeding \\w* |
| regexplib/strings.js:14:107:14:109 | \\w* | Strings starting with 'AAA' and with many repetitions of 'A' can start matching anywhere after the start of the preceeding \\w* |
| regexplib/strings.js:19:31:19:57 | [a-zæøå0-9]+ | Strings starting with '#@' and with many repetitions of '##' can start matching anywhere after the start of the preceeding [a-zæøå0-9]+ |
+| regexplib/strings.js:19:69:19:95 | [a-zæøå0-9]+ | Strings starting with '#@#' and with many repetitions of '##' can start matching anywhere after the start of the preceeding [a-zæøå0-9]+ |
| regexplib/strings.js:20:3:20:20 | ((\\\\")\|[^"(\\\\")])+ | Strings starting with '"' and with many repetitions of '\\\\"' can start matching anywhere after the start of the preceeding "((\\\\")\|[^"(\\\\")])+" |
| regexplib/strings.js:21:3:21:7 | [^>]+ | Strings starting with '<' and with many repetitions of '<' can start matching anywhere after the start of the preceeding <[^>]+> |
| regexplib/strings.js:23:3:23:20 | ((\\\\")\|[^"(\\\\")])+ | Strings starting with '"' and with many repetitions of '\\\\"' can start matching anywhere after the start of the preceeding "((\\\\")\|[^"(\\\\")])+" |
@@ -314,6 +324,7 @@
| regexplib/strings.js:48:3:48:12 | [^\\.\\?\\!]* | Strings with many repetitions of ' ' can start matching anywhere after the start of the preceeding ([^\\.\\?\\!]*)[\\.\\?\\!] |
| regexplib/strings.js:49:3:49:5 | \\S+ | Strings with many repetitions of '!' can start matching anywhere after the start of the preceeding (\\S+)\\x20{2,}(?=\\S+) |
| regexplib/strings.js:53:25:53:33 | [a-z0-9]+ | Strings with many repetitions of '0' can start matching anywhere after the start of the preceeding [a-z0-9]+ |
+| regexplib/strings.js:53:44:53:52 | [a-z0-9]+ | Strings with many repetitions of '00' can start matching anywhere after the start of the preceeding [a-z0-9]+ |
| regexplib/strings.js:53:65:53:73 | [a-z0-9]+ | Strings with many repetitions of '0' can start matching anywhere after the start of the preceeding [a-z0-9]+ |
| regexplib/strings.js:54:20:54:22 | \\w+ | Strings with many repetitions of 'a' can start matching anywhere after the start of the preceeding (NOT)?(\\s*\\(*)\\s*(\\w+)\\s*(=\|<>\|<\|>\|LIKE\|IN)\\s*(\\(([^\\)]*)\\)\|'([^']*)'\|(-?\\d*\\.?\\d+))(\\s*\\)*\\s*)(AND\|OR)? |
| regexplib/strings.js:56:52:56:53 | .+ | Strings starting with 'PRN.' and with many repetitions of '.' can start matching anywhere after the start of the preceeding .* |
@@ -519,3 +530,5 @@
| tst.js:399:6:399:12 | (d\|dd)* | Strings with many repetitions of 'd' can start matching anywhere after the start of the preceeding ((c\|cc)*\|(d\|dd)*\|(e\|ee)*)f$ |
| tst.js:400:6:401:1 | (e\|ee)* | Strings with many repetitions of 'e' can start matching anywhere after the start of the preceeding ((c\|cc)*\|(d\|dd)*\|(e\|ee)*)f$ |
| tst.js:404:6:405:7 | (g\|gg)* | Strings with many repetitions of 'g' can start matching anywhere after the start of the preceeding (g\|gg)*h$ |
+| tst.js:407:128:407:129 | * | Strings starting with '0/*' and with many repetitions of ' ' can start matching anywhere after the start of the preceeding \\s* |
+| tst.js:409:23:409:29 | [\\w.-]* | Strings starting with '//' and with many repetitions of '//' can start matching anywhere after the start of the preceeding (\\/(?:\\/[\\w.-]*)*){0,1}:([\\w.-]+) |
diff --git a/javascript/ql/test/query-tests/Performance/ReDoS/ReDoS.expected b/javascript/ql/test/query-tests/Performance/ReDoS/ReDoS.expected
index 7319e306e62..f5d5efa7c9e 100644
--- a/javascript/ql/test/query-tests/Performance/ReDoS/ReDoS.expected
+++ b/javascript/ql/test/query-tests/Performance/ReDoS/ReDoS.expected
@@ -29,12 +29,14 @@
| regexplib/email.js:5:24:5:35 | [a-zA-Z0-9]+ | This part of the regular expression may cause exponential backtracking on strings starting with '0' and containing many repetitions of '0'. |
| regexplib/email.js:5:63:5:74 | [a-zA-Z0-9]+ | This part of the regular expression may cause exponential backtracking on strings starting with '0@0' and containing many repetitions of '0'. |
| regexplib/email.js:6:10:6:35 | (?:[a-zA-Z0-9][\\.\\-\\+_]?)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '0'. |
+| regexplib/email.js:6:60:6:88 | (?:[a-zA-Z0-9][\\.\\-_]?){0,62} | This part of the regular expression may cause exponential backtracking on strings starting with '0@' and containing many repetitions of '0'. |
| regexplib/email.js:13:36:13:44 | [a-zA-Z]* | This part of the regular expression may cause exponential backtracking on strings starting with 'A' and containing many repetitions of 'A'. |
| regexplib/email.js:25:67:25:78 | [a-zA-Z0-9]+ | This part of the regular expression may cause exponential backtracking on strings starting with '0' and containing many repetitions of '0'. |
| regexplib/email.js:25:106:25:117 | [a-zA-Z0-9]+ | This part of the regular expression may cause exponential backtracking on strings starting with '0@0' and containing many repetitions of '0'. |
| regexplib/email.js:25:212:25:223 | [a-zA-Z0-9]+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '0'. |
| regexplib/email.js:25:251:25:262 | [a-zA-Z0-9]+ | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '0'. |
| regexplib/email.js:32:10:32:25 | (?:\\w[\\.\\-\\+]?)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'a'. |
+| regexplib/email.js:32:41:32:61 | (?:\\w[\\.\\-\\+]?){0,62} | This part of the regular expression may cause exponential backtracking on strings starting with 'a@' and containing many repetitions of 'a'. |
| regexplib/email.js:33:16:33:22 | [-.\\w]* | This part of the regular expression may cause exponential backtracking on strings starting with '0' and containing many repetitions of '0'. |
| regexplib/email.js:33:38:33:51 | ([0-9a-zA-Z])+ | This part of the regular expression may cause exponential backtracking on strings starting with '0@' and containing many repetitions of '00.'. |
| regexplib/email.js:33:53:33:58 | [-\\w]* | This part of the regular expression may cause exponential backtracking on strings starting with '0@0' and containing many repetitions of '0'. |
@@ -45,9 +47,13 @@
| regexplib/markup.js:13:6:13:12 | [^"']+? | This part of the regular expression may cause exponential backtracking on strings starting with '<' and containing many repetitions of '!'. |
| regexplib/markup.js:13:14:13:16 | .+? | This part of the regular expression may cause exponential backtracking on strings starting with '<' and containing many repetitions of 'a"'. |
| regexplib/markup.js:17:17:17:19 | .*? | This part of the regular expression may cause exponential backtracking on strings starting with '= 30.
+ * 30 has been chosen as a threshold because for exponential blowup 2^30 is enough to get a decent DOS attack.
+ */
+private class EffectivelyPlus extends RegExpTerm {
+ EffectivelyPlus() {
+ this instanceof RegExpPlus
+ or
+ exists(RegExpRange range | range.getLowerBound() = 1 and range.getUpperBound() >= 30 |
+ this = range
+ )
+ }
+}
+
+/**
+ * A RegExp term that acts like a star.
+ * Either it's a RegExpStar, or it is a range {0,X} where X is >= 30.
+ */
+private class EffectivelyStar extends RegExpTerm {
+ EffectivelyStar() {
+ this instanceof RegExpStar
+ or
+ exists(RegExpRange range | range.getLowerBound() = 0 and range.getUpperBound() >= 30 |
+ this = range
+ )
+ }
+}
+
+/**
+ * A RegExp term that acts like a question mark.
+ * Either it's a RegExpQuestion, or it is a range {0,1}.
+ */
+private class EffectivelyQuestion extends RegExpTerm {
+ EffectivelyQuestion() {
+ this instanceof RegExpOpt
+ or
+ exists(RegExpRange range | range.getLowerBound() = 0 and range.getUpperBound() = 1 |
+ this = range
+ )
+ }
+}
+
/**
* Gets the state before matching `t`.
*/
@@ -559,14 +604,14 @@ State after(RegExpTerm t) {
or
exists(RegExpGroup grp | t = grp.getAChild() | result = after(grp))
or
- exists(RegExpStar star | t = star.getAChild() | result = before(star))
+ exists(EffectivelyStar star | t = star.getAChild() | result = before(star))
or
- exists(RegExpPlus plus | t = plus.getAChild() |
+ exists(EffectivelyPlus plus | t = plus.getAChild() |
result = before(plus) or
result = after(plus)
)
or
- exists(RegExpOpt opt | t = opt.getAChild() | result = after(opt))
+ exists(EffectivelyQuestion opt | t = opt.getAChild() | result = after(opt))
or
exists(RegExpRoot root | t = root | result = AcceptAnySuffix(root))
}
@@ -617,15 +662,17 @@ predicate delta(State q1, EdgeLabel lbl, State q2) {
or
exists(RegExpGroup grp | lbl = Epsilon() | q1 = before(grp) and q2 = before(grp.getChild(0)))
or
- exists(RegExpStar star | lbl = Epsilon() |
+ exists(EffectivelyStar star | lbl = Epsilon() |
q1 = before(star) and q2 = before(star.getChild(0))
or
q1 = before(star) and q2 = after(star)
)
or
- exists(RegExpPlus plus | lbl = Epsilon() | q1 = before(plus) and q2 = before(plus.getChild(0)))
+ exists(EffectivelyPlus plus | lbl = Epsilon() |
+ q1 = before(plus) and q2 = before(plus.getChild(0))
+ )
or
- exists(RegExpOpt opt | lbl = Epsilon() |
+ exists(EffectivelyQuestion opt | lbl = Epsilon() |
q1 = before(opt) and q2 = before(opt.getChild(0))
or
q1 = before(opt) and q2 = after(opt)
diff --git a/ruby/ql/lib/codeql/ruby/security/performance/ReDoSUtil.qll b/ruby/ql/lib/codeql/ruby/security/performance/ReDoSUtil.qll
index cec3b654acd..d57e7987ba3 100644
--- a/ruby/ql/lib/codeql/ruby/security/performance/ReDoSUtil.qll
+++ b/ruby/ql/lib/codeql/ruby/security/performance/ReDoSUtil.qll
@@ -539,6 +539,49 @@ private class EdgeLabel extends TInputSymbol {
}
}
+/**
+ * A RegExp term that acts like a plus.
+ * Either it's a RegExpPlus, or it is a range {1,X} where X is >= 30.
+ * 30 has been chosen as a threshold because for exponential blowup 2^30 is enough to get a decent DOS attack.
+ */
+private class EffectivelyPlus extends RegExpTerm {
+ EffectivelyPlus() {
+ this instanceof RegExpPlus
+ or
+ exists(RegExpRange range | range.getLowerBound() = 1 and range.getUpperBound() >= 30 |
+ this = range
+ )
+ }
+}
+
+/**
+ * A RegExp term that acts like a star.
+ * Either it's a RegExpStar, or it is a range {0,X} where X is >= 30.
+ */
+private class EffectivelyStar extends RegExpTerm {
+ EffectivelyStar() {
+ this instanceof RegExpStar
+ or
+ exists(RegExpRange range | range.getLowerBound() = 0 and range.getUpperBound() >= 30 |
+ this = range
+ )
+ }
+}
+
+/**
+ * A RegExp term that acts like a question mark.
+ * Either it's a RegExpQuestion, or it is a range {0,1}.
+ */
+private class EffectivelyQuestion extends RegExpTerm {
+ EffectivelyQuestion() {
+ this instanceof RegExpOpt
+ or
+ exists(RegExpRange range | range.getLowerBound() = 0 and range.getUpperBound() = 1 |
+ this = range
+ )
+ }
+}
+
/**
* Gets the state before matching `t`.
*/
@@ -559,14 +602,14 @@ State after(RegExpTerm t) {
or
exists(RegExpGroup grp | t = grp.getAChild() | result = after(grp))
or
- exists(RegExpStar star | t = star.getAChild() | result = before(star))
+ exists(EffectivelyStar star | t = star.getAChild() | result = before(star))
or
- exists(RegExpPlus plus | t = plus.getAChild() |
+ exists(EffectivelyPlus plus | t = plus.getAChild() |
result = before(plus) or
result = after(plus)
)
or
- exists(RegExpOpt opt | t = opt.getAChild() | result = after(opt))
+ exists(EffectivelyQuestion opt | t = opt.getAChild() | result = after(opt))
or
exists(RegExpRoot root | t = root | result = AcceptAnySuffix(root))
}
@@ -617,15 +660,17 @@ predicate delta(State q1, EdgeLabel lbl, State q2) {
or
exists(RegExpGroup grp | lbl = Epsilon() | q1 = before(grp) and q2 = before(grp.getChild(0)))
or
- exists(RegExpStar star | lbl = Epsilon() |
+ exists(EffectivelyStar star | lbl = Epsilon() |
q1 = before(star) and q2 = before(star.getChild(0))
or
q1 = before(star) and q2 = after(star)
)
or
- exists(RegExpPlus plus | lbl = Epsilon() | q1 = before(plus) and q2 = before(plus.getChild(0)))
+ exists(EffectivelyPlus plus | lbl = Epsilon() |
+ q1 = before(plus) and q2 = before(plus.getChild(0))
+ )
or
- exists(RegExpOpt opt | lbl = Epsilon() |
+ exists(EffectivelyQuestion opt | lbl = Epsilon() |
q1 = before(opt) and q2 = before(opt.getChild(0))
or
q1 = before(opt) and q2 = after(opt)
diff --git a/ruby/ql/test/query-tests/security/cwe-1333-exponential-redos/tst.rb b/ruby/ql/test/query-tests/security/cwe-1333-exponential-redos/tst.rb
index 0d4e893c660..0bebe14c536 100644
--- a/ruby/ql/test/query-tests/security/cwe-1333-exponential-redos/tst.rb
+++ b/ruby/ql/test/query-tests/security/cwe-1333-exponential-redos/tst.rb
@@ -362,11 +362,11 @@ bad84 = /^((?:a{0|-)|\w\{\d)+X$/
bad85 = /^((?:a{0,|-)|\w\{\d,)+X$/
bad86 = /^((?:a{0,2|-)|\w\{\d,\d)+X$/
-# GOOD:
-good42 = /^((?:a{0,2}|-)|\w\{\d,\d\})+X$/
+# NOT GOOD
+bad87 = /^((?:a{0,2}|-)|\w\{\d,\d\})+X$/
# NOT GOOD
-bad87 = /^X(\u0061|a)*Y$/
+bad88 = /^X(\u0061|a)*Y$/
# GOOD
good43 = /^X(\u0061|b)+Y$/