From 602538d1c1024dc794cb002d9e501680c2d8fab9 Mon Sep 17 00:00:00 2001
From: Arthur Baars
Date: Mon, 7 Mar 2022 16:08:42 +0100
Subject: [PATCH 01/13] Ruby: add RegExpPatterns module
---
.../ruby/security/performance/RegExpTreeView.qll | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll b/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll
index f49592f96aa..c70e7c0dd7a 100644
--- a/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll
+++ b/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll
@@ -59,6 +59,19 @@ module RegExpFlags {
}
}
+/**
+ * Provides regular expression patterns.
+ */
+module RegExpPatterns {
+ /**
+ * Gets a pattern that matches common top-level domain names in lower case.
+ */
+ string commonTLD() {
+ // according to ranking by http://google.com/search?q=site:.<>
+ result = "(?:com|org|edu|gov|uk|net|io)(?![a-z0-9])"
+ }
+}
+
/**
* An element containing a regular expression term, that is, either
* a string literal (parsed as a regular expression)
From 832c9c4b0bdaaabd79d6b2f02d99bcad31db4867 Mon Sep 17 00:00:00 2001
From: Arthur Baars
Date: Tue, 1 Mar 2022 12:22:07 +0100
Subject: [PATCH 02/13] Ruby: copy IncompleteHostnameRegExp files from
JavaScript
---
.../security/cwe-020/HostnameRegexpShared.qll | 109 ++++++++++++++++++
.../cwe-020/IncompleteHostnameRegExp.qhelp | 73 ++++++++++++
.../cwe-020/IncompleteHostnameRegExp.ql | 107 +++++++++++++++++
.../examples/IncompleteHostnameRegExp.rb | 9 ++
.../IncompleteHostnameRegExp.expected | 26 +++++
.../IncompleteHostnameRegExp.qlref | 1 +
.../IncompleteHostnameRegExp/hosttest.rb | 23 ++++
.../tst-IncompleteHostnameRegExp.rb | 60 ++++++++++
8 files changed, 408 insertions(+)
create mode 100644 ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll
create mode 100644 ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.qhelp
create mode 100644 ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.ql
create mode 100644 ruby/ql/src/queries/security/cwe-020/examples/IncompleteHostnameRegExp.rb
create mode 100644 ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected
create mode 100644 ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.qlref
create mode 100644 ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/hosttest.rb
create mode 100644 ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/tst-IncompleteHostnameRegExp.rb
diff --git a/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll b/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll
new file mode 100644
index 00000000000..97d75d4a524
--- /dev/null
+++ b/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll
@@ -0,0 +1,109 @@
+/**
+ * Provides predicates for reasoning about regular expressions
+ * that match URLs and hostname patterns.
+ */
+
+import javascript
+
+/**
+ * Holds if the given constant is unlikely to occur in the origin part of a URL.
+ */
+predicate isConstantInvalidInsideOrigin(RegExpConstant term) {
+ // Look for any of these cases:
+ // - A character that can't occur in the origin
+ // - Two dashes in a row
+ // - A colon that is not part of port or scheme separator
+ // - A slash that is not part of scheme separator
+ term.getValue().regexpMatch(".*(?:[^a-zA-Z0-9.:/-]|--|:[^0-9/]|(?
+
+
+
+
+
+ Sanitizing untrusted URLs is an important technique for
+ preventing attacks such as request forgeries and malicious
+ redirections. Often, this is done by checking that the host of a URL
+ is in a set of allowed hosts.
+
+
+
+
+
+ If a regular expression implements such a check, it is
+ easy to accidentally make the check too permissive by not escaping the
+ . meta-characters appropriately.
+
+ Even if the check is not used in a security-critical
+ context, the incomplete check may still cause undesirable behaviors
+ when it accidentally succeeds.
+
+
+
+
+
+
+
+ Escape all meta-characters appropriately when constructing
+ regular expressions for security checks, pay special attention to the
+ . meta-character.
+
+
+
+
+
+
+
+
+ The following example code checks that a URL redirection
+ will reach the example.com domain, or one of its
+ subdomains.
+
+
+
+
+
+
+
+ The check is however easy to bypass because the unescaped
+ . allows for any character before
+ example.com, effectively allowing the redirect to go to
+ an attacker-controlled domain such as wwwXexample.com.
+
+
+
+
+ Address this vulnerability by escaping .
+ appropriately: let regex = /((www|beta)\.)?example\.com/.
+
+
+
+
+
+
+ MDN: Regular Expressions
+ OWASP: SSRF
+ OWASP: XSS Unvalidated Redirects and Forwards Cheat Sheet.
+
+
diff --git a/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.ql b/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.ql
new file mode 100644
index 00000000000..bd095887011
--- /dev/null
+++ b/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.ql
@@ -0,0 +1,107 @@
+/**
+ * @name Incomplete regular expression for hostnames
+ * @description Matching a URL or hostname against a regular expression that contains an unescaped dot as part of the hostname might match more hostnames than expected.
+ * @kind problem
+ * @problem.severity warning
+ * @security-severity 7.8
+ * @precision high
+ * @id js/incomplete-hostname-regexp
+ * @tags correctness
+ * security
+ * external/cwe/cwe-020
+ */
+
+import javascript
+import semmle.javascript.CharacterEscapes
+import HostnameRegexpShared
+
+/**
+ * Holds if `term` occurs inside a quantifier or alternative (and thus
+ * can not be expected to correspond to a unique match), or as part of
+ * a lookaround assertion (which are rarely used for capture groups).
+ */
+predicate isInsideChoiceOrSubPattern(RegExpTerm term) {
+ exists(RegExpParent parent | parent = term.getParent() |
+ parent instanceof RegExpAlt
+ or
+ parent instanceof RegExpQuantifier
+ or
+ parent instanceof RegExpSubPattern
+ or
+ isInsideChoiceOrSubPattern(parent)
+ )
+}
+
+/**
+ * Holds if `group` is likely to be used as a capture group.
+ */
+predicate isLikelyCaptureGroup(RegExpGroup group) {
+ group.isCapture() and
+ not isInsideChoiceOrSubPattern(group)
+}
+
+/**
+ * Holds if `seq` contains two consecutive dots `..` or escaped dots.
+ *
+ * At least one of these dots is not intended to be a subdomain separator,
+ * so we avoid flagging the pattern in this case.
+ */
+predicate hasConsecutiveDots(RegExpSequence seq) {
+ exists(int i |
+ isDotLike(seq.getChild(i)) and
+ isDotLike(seq.getChild(i + 1))
+ )
+}
+
+predicate isIncompleteHostNameRegExpPattern(RegExpTerm regexp, RegExpSequence seq, string msg) {
+ seq = regexp.getAChild*() and
+ exists(RegExpDot unescapedDot, int i, string hostname |
+ hasTopLevelDomainEnding(seq, i) and
+ not isConstantInvalidInsideOrigin(seq.getChild([0 .. i - 1]).getAChild*()) and
+ not isLikelyCaptureGroup(seq.getChild([i .. seq.getNumChild() - 1]).getAChild*()) and
+ unescapedDot = seq.getChild([0 .. i - 1]).getAChild*() and
+ unescapedDot != seq.getChild(i - 1) and // Should not be the '.' immediately before the TLD
+ not hasConsecutiveDots(unescapedDot.getParent()) and
+ hostname =
+ seq.getChild(i - 2).getRawValue() + seq.getChild(i - 1).getRawValue() +
+ seq.getChild(i).getRawValue()
+ |
+ if unescapedDot.getParent() instanceof RegExpQuantifier
+ then
+ // `.*\.example.com` can match `evil.com/?x=.example.com`
+ //
+ // This problem only occurs when the pattern is applied against a full URL, not just a hostname/origin.
+ // We therefore check if the pattern includes a suffix after the TLD, such as `.*\.example.com/`.
+ // Note that a post-anchored pattern (`.*\.example.com$`) will usually fail to match a full URL,
+ // and patterns with neither a suffix nor an anchor fall under the purview of MissingRegExpAnchor.
+ seq.getChild(0) instanceof RegExpCaret and
+ not seq.getAChild() instanceof RegExpDollar and
+ seq.getChild([i .. i + 1]).(RegExpConstant).getValue().regexpMatch(".*[/?#].*") and
+ msg =
+ "has an unrestricted wildcard '" + unescapedDot.getParent().(RegExpQuantifier).getRawValue()
+ + "' which may cause '" + hostname +
+ "' to be matched anywhere in the URL, outside the hostname."
+ else
+ msg =
+ "has an unescaped '.' before '" + hostname +
+ "', so it might match more hosts than expected."
+ )
+}
+
+from
+ RegExpPatternSource re, RegExpTerm regexp, RegExpSequence hostSequence, string msg, string kind,
+ DataFlow::Node aux
+where
+ regexp = re.getRegExpTerm() and
+ isIncompleteHostNameRegExpPattern(regexp, hostSequence, msg) and
+ (
+ if re.getAParse() != re
+ then (
+ kind = "string, which is used as a regular expression $@," and
+ aux = re.getAParse()
+ ) else (
+ kind = "regular expression" and aux = re
+ )
+ ) and
+ not CharacterEscapes::hasALikelyRegExpPatternMistake(re)
+select hostSequence, "This " + kind + " " + msg, aux, "here"
diff --git a/ruby/ql/src/queries/security/cwe-020/examples/IncompleteHostnameRegExp.rb b/ruby/ql/src/queries/security/cwe-020/examples/IncompleteHostnameRegExp.rb
new file mode 100644
index 00000000000..11d11a97ca4
--- /dev/null
+++ b/ruby/ql/src/queries/security/cwe-020/examples/IncompleteHostnameRegExp.rb
@@ -0,0 +1,9 @@
+app.get('/some/path', function(req, res) {
+ let url = req.param('url'),
+ host = urlLib.parse(url).host;
+ // BAD: the host of `url` may be controlled by an attacker
+ let regex = /^((www|beta).)?example.com/;
+ if (host.match(regex)) {
+ res.redirect(url);
+ }
+});
diff --git a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected
new file mode 100644
index 00000000000..13950be73ba
--- /dev/null
+++ b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected
@@ -0,0 +1,26 @@
+| tst-IncompleteHostnameRegExp.js:3:3:3:28 | ^http:\\/\\/test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:3:2:3:29 | /^http: ... le.com/ | here |
+| tst-IncompleteHostnameRegExp.js:5:3:5:28 | ^http:\\/\\/test.example.net | This regular expression has an unescaped '.' before 'example.net', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:5:2:5:29 | /^http: ... le.net/ | here |
+| tst-IncompleteHostnameRegExp.js:6:3:6:42 | ^http:\\/\\/test.(example-a\|example-b).com | This regular expression has an unescaped '.' before '(example-a\|example-b).com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:6:2:6:43 | /^http: ... b).com/ | here |
+| tst-IncompleteHostnameRegExp.js:7:3:7:30 | ^http:\\/\\/(.+).example.com\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:7:2:7:31 | /^http: ... .com\\// | here |
+| tst-IncompleteHostnameRegExp.js:7:3:7:30 | ^http:\\/\\/(.+).example.com\\/ | This regular expression has an unrestricted wildcard '.+' which may cause 'example.com' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.js:7:2:7:31 | /^http: ... .com\\// | here |
+| tst-IncompleteHostnameRegExp.js:10:3:10:36 | ^http:\\/\\/test.example.com\\/(?:.*) | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:10:2:10:37 | /^http: ... (?:.*)/ | here |
+| tst-IncompleteHostnameRegExp.js:11:14:11:37 | ^http://test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:11:13:11:38 | "^http: ... le.com" | here |
+| tst-IncompleteHostnameRegExp.js:12:15:12:38 | ^http://test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:12:14:12:39 | "^http: ... le.com" | here |
+| tst-IncompleteHostnameRegExp.js:15:23:15:46 | ^http://test.example.com | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:15:13:15:50 | id(id(i ... com"))) | here |
+| tst-IncompleteHostnameRegExp.js:19:18:19:34 | ^test.example.com | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:20:13:20:26 | `${hostname}$` | here |
+| tst-IncompleteHostnameRegExp.js:22:28:22:44 | test.example.com$ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:23:13:23:27 | domain.hostname | here |
+| tst-IncompleteHostnameRegExp.js:28:24:28:40 | test.example.com$ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:26:21:26:35 | domain.hostname | here |
+| tst-IncompleteHostnameRegExp.js:30:31:30:47 | test.example.com$ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:32:21:32:35 | domain.hostname | here |
+| tst-IncompleteHostnameRegExp.js:37:3:37:53 | ^(https?:)?\\/\\/((service\|www).)?example.com(?=$\|\\/) | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:37:2:37:54 | /^(http ... =$\|\\/)/ | here |
+| tst-IncompleteHostnameRegExp.js:38:3:38:43 | ^(http\|https):\\/\\/www.example.com\\/p\\/f\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:38:2:38:44 | /^(http ... p\\/f\\// | here |
+| tst-IncompleteHostnameRegExp.js:39:5:39:30 | http:\\/\\/sub.example.com\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:39:2:39:33 | /^(http ... om\\/)/g | here |
+| tst-IncompleteHostnameRegExp.js:40:3:40:29 | ^https?:\\/\\/api.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:40:2:40:30 | /^https ... le.com/ | here |
+| tst-IncompleteHostnameRegExp.js:41:42:41:48 | ^https?://.+\\.example\\.com/ | This regular expression has an unrestricted wildcard '.+' which may cause 'example\\.com/' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.js:41:13:41:71 | '^http: ... \\.com/' | here |
+| tst-IncompleteHostnameRegExp.js:43:3:43:32 | ^https:\\/\\/[a-z]*.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:43:2:43:33 | /^https ... e.com$/ | here |
+| tst-IncompleteHostnameRegExp.js:44:32:44:45 | .+.example.net | This regular expression has an unescaped '.' before 'example.net', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:44:9:44:101 | '^proto ... ernal)' | here |
+| tst-IncompleteHostnameRegExp.js:44:47:44:62 | .+.example-a.com | This regular expression has an unescaped '.' before 'example-a.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:44:9:44:101 | '^proto ... ernal)' | here |
+| tst-IncompleteHostnameRegExp.js:44:64:44:79 | .+.example-b.com | This regular expression has an unescaped '.' before 'example-b.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:44:9:44:101 | '^proto ... ernal)' | here |
+| tst-IncompleteHostnameRegExp.js:48:42:48:47 | ^https?://.+.example\\.com/ | This regular expression has an unescaped '.' before 'example\\.com/', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:48:13:48:69 | '^http: ... \\.com/' | here |
+| tst-IncompleteHostnameRegExp.js:48:42:48:47 | ^https?://.+.example\\.com/ | This regular expression has an unrestricted wildcard '.+' which may cause 'example\\.com/' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.js:48:13:48:69 | '^http: ... \\.com/' | here |
+| tst-IncompleteHostnameRegExp.js:53:14:53:35 | test.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:53:13:53:36 | 'test.' ... e.com$' | here |
+| tst-IncompleteHostnameRegExp.js:59:5:59:20 | foo.example\\.com | This regular expression has an unescaped '.' before 'example\\.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:59:2:59:32 | /^(foo. ... ever)$/ | here |
diff --git a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.qlref b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.qlref
new file mode 100644
index 00000000000..e818d947252
--- /dev/null
+++ b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.qlref
@@ -0,0 +1 @@
+Security/CWE-020/IncompleteHostnameRegExp.ql
\ No newline at end of file
diff --git a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/hosttest.rb b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/hosttest.rb
new file mode 100644
index 00000000000..ca7f7094a8c
--- /dev/null
+++ b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/hosttest.rb
@@ -0,0 +1,23 @@
+from flask import Flask, request, redirect
+import re
+
+app = Flask(__name__)
+
+UNSAFE_REGEX = re.compile("(www|beta).example.com/")
+SAFE_REGEX = re.compile(r"(www|beta)\.example\.com/")
+
+@app.route('/some/path/bad')
+def unsafe(request):
+ target = request.args.get('target', '')
+ if UNSAFE_REGEX.match(target):
+ return redirect(target)
+
+@app.route('/some/path/good')
+def safe(request):
+ target = request.args.get('target', '')
+ if SAFE_REGEX.match(target):
+ return redirect(target)
+
+# FP reported in https://github.com/github/codeql/issues/3712
+# This does not define a regex (but could be used by other code to do so)
+escaped = re.escape("https://www.humblebundle.com/home/library")
diff --git a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/tst-IncompleteHostnameRegExp.rb b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/tst-IncompleteHostnameRegExp.rb
new file mode 100644
index 00000000000..df887092d23
--- /dev/null
+++ b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/tst-IncompleteHostnameRegExp.rb
@@ -0,0 +1,60 @@
+(function() {
+ /^http:\/\/example.com/; // OK
+ /^http:\/\/test.example.com/; // NOT OK
+ /^http:\/\/test\\.example.com/; // OK
+ /^http:\/\/test.example.net/; // NOT OK
+ /^http:\/\/test.(example-a|example-b).com/; // NOT OK
+ /^http:\/\/(.+).example.com\//; // NOT OK
+ /^http:\/\/(\\.+)\\.example.com/; // OK
+ /^http:\/\/(?:.+)\\.test\\.example.com\//; // NOT OK
+ /^http:\/\/test.example.com\/(?:.*)/; // OK
+ new RegExp("^http://test.example.com"); // NOT OK
+ if (s.match("^http://test.example.com")) {} // NOT OK
+
+ function id(e) { return e; }
+ new RegExp(id(id(id("^http://test.example.com")))); // NOT OK
+
+ new RegExp(`test.example.com$`); // NOT OK
+
+ let hostname = '^test.example.com'; // NOT OK
+ new RegExp(`${hostname}$`);
+
+ let domain = { hostname: 'test.example.com$' }; // NOT OK
+ new RegExp(domain.hostname);
+
+ function convert1(domain) {
+ return new RegExp(domain.hostname);
+ }
+ convert1({ hostname: 'test.example.com$' }); // NOT OK
+
+ let domains = [ { hostname: 'test.example.com$' } ]; // NOT OK
+ function convert2(domain) {
+ return new RegExp(domain.hostname);
+ }
+ domains.map(d => convert2(d));
+
+ /^(.+\.(?:example-a|example-b)\.com)\//; // NOT OK
+ /^(https?:)?\/\/((service|www).)?example.com(?=$|\/)/; // NOT OK
+ /^(http|https):\/\/www.example.com\/p\/f\//; // NOT OK
+ /^(http:\/\/sub.example.com\/)/g; // NOT OK
+ /^https?:\/\/api.example.com/; // NOT OK
+ new RegExp('^http://localhost:8000|' + '^https?://.+\\.example\\.com/'); // NOT OK
+ new RegExp('^http[s]?:\/\/?sub1\\.sub2\\.example\\.com\/f\/(.+)'); // NOT OK
+ /^https:\/\/[a-z]*.example.com$/; // NOT OK
+ RegExp('^protos?://(localhost|.+.example.net|.+.example-a.com|.+.example-b.com|.+.example.internal)'); // NOT OK
+
+ /^(example.dev|example.com)/; // OK
+
+ new RegExp('^http://localhost:8000|' + '^https?://.+.example\\.com/'); // NOT OK
+
+ var primary = 'example.com$';
+ new RegExp('test.' + primary); // NOT OK, but not detected
+
+ new RegExp('test.' + 'example.com$'); // NOT OK
+
+ new RegExp('^http://test\.example.com'); // NOT OK, but flagged by js/useless-regexp-character-escape
+
+ /^http:\/\/(..|...)\.example\.com\/index\.html/; // OK, wildcards are intentional
+ /^http:\/\/.\.example\.com\/index\.html/; // OK, the wildcard is intentional
+ /^(foo.example\.com|whatever)$/; // kinda OK - one disjunction doesn't even look like a hostname
+});
From 9e8930c19278ca9fe4ef77115f32c6ace3a0f17b Mon Sep 17 00:00:00 2001
From: Arthur Baars
Date: Wed, 9 Feb 2022 18:07:14 +0100
Subject: [PATCH 03/13] Ruby: IncompleteHostnameRegExp.ql
---
.../CWE-020/IncompleteHostnameRegExp.qhelp | 2 +-
.../CWE-020/IncompleteHostnameRegExp.qhelp | 2 +-
.../security/performance/RegExpTreeView.qll | 3 +
.../2022-02-10-incomplete-hostname-regexp.md | 4 +
.../security/cwe-020/HostnameRegexpShared.qll | 5 +-
.../cwe-020/IncompleteHostnameRegExp.qhelp | 7 +-
.../cwe-020/IncompleteHostnameRegExp.ql | 10 +-
.../examples/IncompleteHostnameRegExp.rb | 22 ++--
.../IncompleteHostnameRegExp.expected | 58 +++++-----
.../IncompleteHostnameRegExp.qlref | 2 +-
.../IncompleteHostnameRegExp/hosttest.rb | 35 +++---
.../tst-IncompleteHostnameRegExp.rb | 101 ++++++++++--------
12 files changed, 137 insertions(+), 114 deletions(-)
create mode 100644 ruby/ql/src/change-notes/2022-02-10-incomplete-hostname-regexp.md
diff --git a/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp b/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp
index a5e0a78b8cc..86637d140df 100644
--- a/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp
+++ b/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp
@@ -30,7 +30,7 @@
Escape all meta-characters appropriately when constructing
- regular expressions for security checks, pay special attention to the
+ regular expressions for security checks, and pay special attention to the
. meta-character.
diff --git a/python/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp b/python/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp
index d112cbca508..66c9dd847c7 100644
--- a/python/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp
+++ b/python/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp
@@ -30,7 +30,7 @@
Escape all meta-characters appropriately when constructing
- regular expressions for security checks, pay special attention to the
+ regular expressions for security checks, and pay special attention to the
. meta-character.
diff --git a/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll b/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll
index c70e7c0dd7a..9043fa949ba 100644
--- a/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll
+++ b/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll
@@ -637,6 +637,9 @@ class RegExpGroup extends RegExpTerm, TRegExpGroup {
*/
int getNumber() { result = re.getGroupNumber(start, end) }
+ /** Holds if this is a capture group. */
+ predicate isCapture() { not exists(this.getNumber()) }
+
/** Holds if this is a named capture group. */
predicate isNamed() { exists(this.getName()) }
diff --git a/ruby/ql/src/change-notes/2022-02-10-incomplete-hostname-regexp.md b/ruby/ql/src/change-notes/2022-02-10-incomplete-hostname-regexp.md
new file mode 100644
index 00000000000..f87676dc188
--- /dev/null
+++ b/ruby/ql/src/change-notes/2022-02-10-incomplete-hostname-regexp.md
@@ -0,0 +1,4 @@
+---
+category: newQuery
+---
+* Added a new query, `rb/incomplete-hostname-regexp`. The query finds instances where a hostname is incompletely sanitized due to an unescaped character in a regular expression.
diff --git a/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll b/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll
index 97d75d4a524..102d719979e 100644
--- a/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll
+++ b/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll
@@ -3,7 +3,8 @@
* that match URLs and hostname patterns.
*/
-import javascript
+private import codeql.ruby.security.performance.RegExpTreeView
+private import codeql.ruby.DataFlow
/**
* Holds if the given constant is unlikely to occur in the origin part of a URL.
@@ -19,7 +20,7 @@ predicate isConstantInvalidInsideOrigin(RegExpConstant term) {
/** Holds if `term` is a dot constant of form `\.` or `[.]`. */
predicate isDotConstant(RegExpTerm term) {
- term.(RegExpCharEscape).getValue() = "."
+ term.(RegExpEscape).getValue() = "."
or
exists(RegExpCharacterClass cls |
term = cls and
diff --git a/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.qhelp b/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.qhelp
index a5e0a78b8cc..cb242aa8c61 100644
--- a/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.qhelp
+++ b/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.qhelp
@@ -30,7 +30,7 @@
Escape all meta-characters appropriately when constructing
- regular expressions for security checks, pay special attention to the
+ regular expressions for security checks, and pay special attention to the
. meta-character.
@@ -46,7 +46,7 @@
-
+
@@ -59,14 +59,13 @@
Address this vulnerability by escaping .
- appropriately: let regex = /((www|beta)\.)?example\.com/.
+ appropriately: regex = /((www|beta)\.)?example\.com/.
- MDN: Regular Expressions
OWASP: SSRF
OWASP: XSS Unvalidated Redirects and Forwards Cheat Sheet.
diff --git a/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.ql b/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.ql
index bd095887011..5526978b07a 100644
--- a/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.ql
+++ b/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.ql
@@ -5,15 +5,16 @@
* @problem.severity warning
* @security-severity 7.8
* @precision high
- * @id js/incomplete-hostname-regexp
+ * @id rb/incomplete-hostname-regexp
* @tags correctness
* security
* external/cwe/cwe-020
*/
-import javascript
-import semmle.javascript.CharacterEscapes
import HostnameRegexpShared
+import codeql.ruby.security.performance.RegExpTreeView
+import codeql.ruby.DataFlow
+import codeql.ruby.AST as AST
/**
* Holds if `term` occurs inside a quantifier or alternative (and thus
@@ -102,6 +103,5 @@ where
) else (
kind = "regular expression" and aux = re
)
- ) and
- not CharacterEscapes::hasALikelyRegExpPatternMistake(re)
+ )
select hostSequence, "This " + kind + " " + msg, aux, "here"
diff --git a/ruby/ql/src/queries/security/cwe-020/examples/IncompleteHostnameRegExp.rb b/ruby/ql/src/queries/security/cwe-020/examples/IncompleteHostnameRegExp.rb
index 11d11a97ca4..b0a64a658dd 100644
--- a/ruby/ql/src/queries/security/cwe-020/examples/IncompleteHostnameRegExp.rb
+++ b/ruby/ql/src/queries/security/cwe-020/examples/IncompleteHostnameRegExp.rb
@@ -1,9 +1,13 @@
-app.get('/some/path', function(req, res) {
- let url = req.param('url'),
- host = urlLib.parse(url).host;
- // BAD: the host of `url` may be controlled by an attacker
- let regex = /^((www|beta).)?example.com/;
- if (host.match(regex)) {
- res.redirect(url);
- }
-});
+class AppController < ApplicationController
+
+ def index
+ url = params[:url]
+ host = URI(url).host
+ # BAD: the host of `url` may be controlled by an attacker
+ regex = /^((www|beta).)?example.com/
+ if host.match(regex)
+ redirect_to url
+ end
+ end
+
+end
diff --git a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected
index 13950be73ba..944cb284f11 100644
--- a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected
+++ b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected
@@ -1,26 +1,32 @@
-| tst-IncompleteHostnameRegExp.js:3:3:3:28 | ^http:\\/\\/test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:3:2:3:29 | /^http: ... le.com/ | here |
-| tst-IncompleteHostnameRegExp.js:5:3:5:28 | ^http:\\/\\/test.example.net | This regular expression has an unescaped '.' before 'example.net', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:5:2:5:29 | /^http: ... le.net/ | here |
-| tst-IncompleteHostnameRegExp.js:6:3:6:42 | ^http:\\/\\/test.(example-a\|example-b).com | This regular expression has an unescaped '.' before '(example-a\|example-b).com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:6:2:6:43 | /^http: ... b).com/ | here |
-| tst-IncompleteHostnameRegExp.js:7:3:7:30 | ^http:\\/\\/(.+).example.com\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:7:2:7:31 | /^http: ... .com\\// | here |
-| tst-IncompleteHostnameRegExp.js:7:3:7:30 | ^http:\\/\\/(.+).example.com\\/ | This regular expression has an unrestricted wildcard '.+' which may cause 'example.com' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.js:7:2:7:31 | /^http: ... .com\\// | here |
-| tst-IncompleteHostnameRegExp.js:10:3:10:36 | ^http:\\/\\/test.example.com\\/(?:.*) | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:10:2:10:37 | /^http: ... (?:.*)/ | here |
-| tst-IncompleteHostnameRegExp.js:11:14:11:37 | ^http://test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:11:13:11:38 | "^http: ... le.com" | here |
-| tst-IncompleteHostnameRegExp.js:12:15:12:38 | ^http://test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:12:14:12:39 | "^http: ... le.com" | here |
-| tst-IncompleteHostnameRegExp.js:15:23:15:46 | ^http://test.example.com | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:15:13:15:50 | id(id(i ... com"))) | here |
-| tst-IncompleteHostnameRegExp.js:19:18:19:34 | ^test.example.com | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:20:13:20:26 | `${hostname}$` | here |
-| tst-IncompleteHostnameRegExp.js:22:28:22:44 | test.example.com$ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:23:13:23:27 | domain.hostname | here |
-| tst-IncompleteHostnameRegExp.js:28:24:28:40 | test.example.com$ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:26:21:26:35 | domain.hostname | here |
-| tst-IncompleteHostnameRegExp.js:30:31:30:47 | test.example.com$ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:32:21:32:35 | domain.hostname | here |
-| tst-IncompleteHostnameRegExp.js:37:3:37:53 | ^(https?:)?\\/\\/((service\|www).)?example.com(?=$\|\\/) | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:37:2:37:54 | /^(http ... =$\|\\/)/ | here |
-| tst-IncompleteHostnameRegExp.js:38:3:38:43 | ^(http\|https):\\/\\/www.example.com\\/p\\/f\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:38:2:38:44 | /^(http ... p\\/f\\// | here |
-| tst-IncompleteHostnameRegExp.js:39:5:39:30 | http:\\/\\/sub.example.com\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:39:2:39:33 | /^(http ... om\\/)/g | here |
-| tst-IncompleteHostnameRegExp.js:40:3:40:29 | ^https?:\\/\\/api.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:40:2:40:30 | /^https ... le.com/ | here |
-| tst-IncompleteHostnameRegExp.js:41:42:41:48 | ^https?://.+\\.example\\.com/ | This regular expression has an unrestricted wildcard '.+' which may cause 'example\\.com/' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.js:41:13:41:71 | '^http: ... \\.com/' | here |
-| tst-IncompleteHostnameRegExp.js:43:3:43:32 | ^https:\\/\\/[a-z]*.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:43:2:43:33 | /^https ... e.com$/ | here |
-| tst-IncompleteHostnameRegExp.js:44:32:44:45 | .+.example.net | This regular expression has an unescaped '.' before 'example.net', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:44:9:44:101 | '^proto ... ernal)' | here |
-| tst-IncompleteHostnameRegExp.js:44:47:44:62 | .+.example-a.com | This regular expression has an unescaped '.' before 'example-a.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:44:9:44:101 | '^proto ... ernal)' | here |
-| tst-IncompleteHostnameRegExp.js:44:64:44:79 | .+.example-b.com | This regular expression has an unescaped '.' before 'example-b.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:44:9:44:101 | '^proto ... ernal)' | here |
-| tst-IncompleteHostnameRegExp.js:48:42:48:47 | ^https?://.+.example\\.com/ | This regular expression has an unescaped '.' before 'example\\.com/', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:48:13:48:69 | '^http: ... \\.com/' | here |
-| tst-IncompleteHostnameRegExp.js:48:42:48:47 | ^https?://.+.example\\.com/ | This regular expression has an unrestricted wildcard '.+' which may cause 'example\\.com/' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.js:48:13:48:69 | '^http: ... \\.com/' | here |
-| tst-IncompleteHostnameRegExp.js:53:14:53:35 | test.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:53:13:53:36 | 'test.' ... e.com$' | here |
-| tst-IncompleteHostnameRegExp.js:59:5:59:20 | foo.example\\.com | This regular expression has an unescaped '.' before 'example\\.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:59:2:59:32 | /^(foo. ... ever)$/ | here |
+| hosttest.rb:1:18:1:41 | (www\|beta).example.com\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | hosttest.rb:1:17:1:42 | /(www\|beta).example.com\\// | here |
+| hosttest.rb:2:33:2:55 | (www\|beta).example.com/ | This regular expression has an unescaped '.' before 'example.com/', so it might match more hosts than expected. | hosttest.rb:2:32:2:56 | "(www\|beta).example.com/" | here |
+| hosttest.rb:3:29:3:51 | (www\|beta).example.com/ | This regular expression has an unescaped '.' before 'example.com/', so it might match more hosts than expected. | hosttest.rb:3:28:3:52 | "(www\|beta).example.com/" | here |
+| tst-IncompleteHostnameRegExp.rb:3:3:3:28 | ^http:\\/\\/test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:3:2:3:29 | /^http:\\/\\/test.example.com/ | here |
+| tst-IncompleteHostnameRegExp.rb:5:3:5:28 | ^http:\\/\\/test.example.net | This regular expression has an unescaped '.' before 'example.net', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:5:2:5:29 | /^http:\\/\\/test.example.net/ | here |
+| tst-IncompleteHostnameRegExp.rb:6:3:6:42 | ^http:\\/\\/test.(example-a\|example-b).com | This regular expression has an unescaped '.' before '(example-a\|example-b).com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:6:2:6:43 | /^http:\\/\\/test.(example-a\|exa.../ | here |
+| tst-IncompleteHostnameRegExp.rb:7:3:7:30 | ^http:\\/\\/(.+).example.com\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:7:2:7:31 | /^http:\\/\\/(.+).example.com\\// | here |
+| tst-IncompleteHostnameRegExp.rb:7:3:7:30 | ^http:\\/\\/(.+).example.com\\/ | This regular expression has an unrestricted wildcard '.+' which may cause 'example.com' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.rb:7:2:7:31 | /^http:\\/\\/(.+).example.com\\// | here |
+| tst-IncompleteHostnameRegExp.rb:9:3:9:39 | ^http:\\/\\/(?:.+)\\.test\\.example.com\\/ | This regular expression has an unrestricted wildcard '.+' which may cause 'example.com' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.rb:9:2:9:40 | /^http:\\/\\/(?:.+)\\.test\\.examp.../ | here |
+| tst-IncompleteHostnameRegExp.rb:10:3:10:36 | ^http:\\/\\/test.example.com\\/(?:.*) | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:10:2:10:37 | /^http:\\/\\/test.example.com\\/(.../ | here |
+| tst-IncompleteHostnameRegExp.rb:11:14:11:37 | ^http://test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:11:13:11:38 | "^http://test.example.com" | here |
+| tst-IncompleteHostnameRegExp.rb:12:15:12:38 | ^http://test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:12:14:12:39 | "^http://test.example.com" | here |
+| tst-IncompleteHostnameRegExp.rb:15:23:15:46 | ^http://test.example.com | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:15:13:15:50 | call to id | here |
+| tst-IncompleteHostnameRegExp.rb:17:14:17:30 | test.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:17:13:17:31 | `test.example.com$` | here |
+| tst-IncompleteHostnameRegExp.rb:19:14:19:30 | ^test.example.com | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:20:13:20:26 | "#{...}$" | here |
+| tst-IncompleteHostnameRegExp.rb:20:14:20:31 | ^test.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:20:13:20:26 | "#{...}$" | here |
+| tst-IncompleteHostnameRegExp.rb:22:24:22:40 | test.example.com$ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:23:13:23:29 | ...[...] | here |
+| tst-IncompleteHostnameRegExp.rb:28:24:28:40 | test.example.com$ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:63:20:63:36 | ...[...] | here |
+| tst-IncompleteHostnameRegExp.rb:30:31:30:47 | test.example.com$ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:32:21:32:35 | domain.hostname | here |
+| tst-IncompleteHostnameRegExp.rb:37:3:37:53 | ^(https?:)?\\/\\/((service\|www).)?example.com(?=$\|\\/) | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:37:2:37:54 | /^(https?:)?\\/\\/((service\|www).../ | here |
+| tst-IncompleteHostnameRegExp.rb:38:3:38:43 | ^(http\|https):\\/\\/www.example.com\\/p\\/f\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:38:2:38:44 | /^(http\|https):\\/\\/www.example.../ | here |
+| tst-IncompleteHostnameRegExp.rb:39:5:39:30 | http:\\/\\/sub.example.com\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:39:2:39:33 | /^(http:\\/\\/sub.example.com\\/)/ | here |
+| tst-IncompleteHostnameRegExp.rb:40:3:40:29 | ^https?:\\/\\/api.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:40:2:40:30 | /^https?:\\/\\/api.example.com/ | here |
+| tst-IncompleteHostnameRegExp.rb:41:42:41:68 | ^https?://.+\\.example\\.com/ | This string, which is used as a regular expression $@, has an unrestricted wildcard '.+' which may cause 'example\\.com/' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.rb:41:13:41:71 | ... + ... | here |
+| tst-IncompleteHostnameRegExp.rb:43:3:43:32 | ^https:\\/\\/[a-z]*.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:43:2:43:33 | /^https:\\/\\/[a-z]*.example.com$/ | here |
+| tst-IncompleteHostnameRegExp.rb:44:40:44:53 | .+.example.net | This regular expression has an unescaped '.' before 'example.net', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:44:17:44:109 | "^protos?://(localhost\|.+.exam..." | here |
+| tst-IncompleteHostnameRegExp.rb:44:55:44:70 | .+.example-a.com | This regular expression has an unescaped '.' before 'example-a.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:44:17:44:109 | "^protos?://(localhost\|.+.exam..." | here |
+| tst-IncompleteHostnameRegExp.rb:44:72:44:87 | .+.example-b.com | This regular expression has an unescaped '.' before 'example-b.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:44:17:44:109 | "^protos?://(localhost\|.+.exam..." | here |
+| tst-IncompleteHostnameRegExp.rb:48:42:48:67 | ^https?://.+.example\\.com/ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example\\.com/', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:48:13:48:69 | ... + ... | here |
+| tst-IncompleteHostnameRegExp.rb:48:42:48:67 | ^https?://.+.example\\.com/ | This string, which is used as a regular expression $@, has an unrestricted wildcard '.+' which may cause 'example\\.com/' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.rb:48:13:48:69 | ... + ... | here |
+| tst-IncompleteHostnameRegExp.rb:53:13:53:36 | test.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:53:13:53:36 | ... + ... | here |
+| tst-IncompleteHostnameRegExp.rb:59:5:59:20 | foo.example\\.com | This regular expression has an unescaped '.' before 'example\\.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:59:2:59:32 | /^(foo.example\\.com\|whatever)$/ | here |
diff --git a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.qlref b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.qlref
index e818d947252..7fd45d159ce 100644
--- a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.qlref
+++ b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.qlref
@@ -1 +1 @@
-Security/CWE-020/IncompleteHostnameRegExp.ql
\ No newline at end of file
+queries/security/cwe-020/IncompleteHostnameRegExp.ql
\ No newline at end of file
diff --git a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/hosttest.rb b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/hosttest.rb
index ca7f7094a8c..5a5c96692ce 100644
--- a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/hosttest.rb
+++ b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/hosttest.rb
@@ -1,23 +1,22 @@
-from flask import Flask, request, redirect
-import re
+UNSAFE_REGEX1 = /(www|beta).example.com\//
+UNSAFE_REGEX2 = Regexp.compile("(www|beta).example.com/")
+UNSAFE_REGEX3 = Regexp.new("(www|beta).example.com/")
+SAFE_REGEX = /(www|beta)\.example\.com\//
-app = Flask(__name__)
+def unsafe
+ target = params[:target]
+ if UNSAFE_REGEX1.match(target)
+ redirect_to target
+ end
+end
-UNSAFE_REGEX = re.compile("(www|beta).example.com/")
-SAFE_REGEX = re.compile(r"(www|beta)\.example\.com/")
-
-@app.route('/some/path/bad')
-def unsafe(request):
- target = request.args.get('target', '')
- if UNSAFE_REGEX.match(target):
- return redirect(target)
-
-@app.route('/some/path/good')
-def safe(request):
- target = request.args.get('target', '')
- if SAFE_REGEX.match(target):
- return redirect(target)
+def safe
+ target = params[:target]
+ if SAFE_REGEX.match(target)
+ redirect_to target
+ end
+end
# FP reported in https://github.com/github/codeql/issues/3712
# This does not define a regex (but could be used by other code to do so)
-escaped = re.escape("https://www.humblebundle.com/home/library")
+escaped = Regexp::escape("https://www.humblebundle.com/home/library")
diff --git a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/tst-IncompleteHostnameRegExp.rb b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/tst-IncompleteHostnameRegExp.rb
index df887092d23..5d7f7afb133 100644
--- a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/tst-IncompleteHostnameRegExp.rb
+++ b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/tst-IncompleteHostnameRegExp.rb
@@ -1,60 +1,67 @@
-(function() {
- /^http:\/\/example.com/; // OK
- /^http:\/\/test.example.com/; // NOT OK
- /^http:\/\/test\\.example.com/; // OK
- /^http:\/\/test.example.net/; // NOT OK
- /^http:\/\/test.(example-a|example-b).com/; // NOT OK
- /^http:\/\/(.+).example.com\//; // NOT OK
- /^http:\/\/(\\.+)\\.example.com/; // OK
- /^http:\/\/(?:.+)\\.test\\.example.com\//; // NOT OK
- /^http:\/\/test.example.com\/(?:.*)/; // OK
- new RegExp("^http://test.example.com"); // NOT OK
- if (s.match("^http://test.example.com")) {} // NOT OK
+def foo
+ /^http:\/\/example.com/; # OK
+ /^http:\/\/test.example.com/; # NOT OK
+ /^http:\/\/test\.example.com/; # OK
+ /^http:\/\/test.example.net/; # NOT OK
+ /^http:\/\/test.(example-a|example-b).com/; # NOT OK
+ /^http:\/\/(.+).example.com\//; # NOT OK
+ /^http:\/\/(\.+)\.example.com/; # OK
+ /^http:\/\/(?:.+)\.test\.example.com\//; # NOT OK
+ /^http:\/\/test.example.com\/(?:.*)/; # OK
+ Regexp.new("^http://test.example.com"); # NOT OK
+ if (s.match("^http://test.example.com")); end # NOT OK
- function id(e) { return e; }
- new RegExp(id(id(id("^http://test.example.com")))); // NOT OK
- new RegExp(`test.example.com$`); // NOT OK
+ Regexp.new(id(id(id("^http://test.example.com")))); # NOT OK
- let hostname = '^test.example.com'; // NOT OK
- new RegExp(`${hostname}$`);
+ Regexp.new(`test.example.com$`); # NOT OK
- let domain = { hostname: 'test.example.com$' }; // NOT OK
- new RegExp(domain.hostname);
+ hostname = '^test.example.com'; # NOT OK
+ Regexp.new("#{hostname}$");
- function convert1(domain) {
- return new RegExp(domain.hostname);
- }
- convert1({ hostname: 'test.example.com$' }); // NOT OK
+ domain = { hostname: 'test.example.com$' }; # NOT OK
+ Regexp.new(domain[:hostname]);
- let domains = [ { hostname: 'test.example.com$' } ]; // NOT OK
- function convert2(domain) {
- return new RegExp(domain.hostname);
- }
- domains.map(d => convert2(d));
- /^(.+\.(?:example-a|example-b)\.com)\//; // NOT OK
- /^(https?:)?\/\/((service|www).)?example.com(?=$|\/)/; // NOT OK
- /^(http|https):\/\/www.example.com\/p\/f\//; // NOT OK
- /^(http:\/\/sub.example.com\/)/g; // NOT OK
- /^https?:\/\/api.example.com/; // NOT OK
- new RegExp('^http://localhost:8000|' + '^https?://.+\\.example\\.com/'); // NOT OK
- new RegExp('^http[s]?:\/\/?sub1\\.sub2\\.example\\.com\/f\/(.+)'); // NOT OK
- /^https:\/\/[a-z]*.example.com$/; // NOT OK
- RegExp('^protos?://(localhost|.+.example.net|.+.example-a.com|.+.example-b.com|.+.example.internal)'); // NOT OK
- /^(example.dev|example.com)/; // OK
- new RegExp('^http://localhost:8000|' + '^https?://.+.example\\.com/'); // NOT OK
+ convert1({ hostname: 'test.example.com$' }); # NOT OK
- var primary = 'example.com$';
- new RegExp('test.' + primary); // NOT OK, but not detected
+ domains = [ { hostname: 'test.example.com$' } ]; # NOT OK
- new RegExp('test.' + 'example.com$'); // NOT OK
- new RegExp('^http://test\.example.com'); // NOT OK, but flagged by js/useless-regexp-character-escape
- /^http:\/\/(..|...)\.example\.com\/index\.html/; // OK, wildcards are intentional
- /^http:\/\/.\.example\.com\/index\.html/; // OK, the wildcard is intentional
- /^(foo.example\.com|whatever)$/; // kinda OK - one disjunction doesn't even look like a hostname
-});
+ domains.map{ |d| convert2(d) };
+
+ /^(.+\.(?:example-a|example-b)\.com)\//; # NOT OK
+ /^(https?:)?\/\/((service|www).)?example.com(?=$|\/)/; # NOT OK
+ /^(http|https):\/\/www.example.com\/p\/f\//; # NOT OK
+ /^(http:\/\/sub.example.com\/)/i; # NOT OK
+ /^https?:\/\/api.example.com/; # NOT OK
+ Regexp.new('^http://localhost:8000|' + "^https?://.+\\.example\\.com/"); # NOT OK
+ Regexp.new("^http[s]?:\/\/?sub1\\.sub2\\.example\\.com\/f\/(.+)"); # NOT OK
+ /^https:\/\/[a-z]*.example.com$/; # NOT OK
+ Regexp.compile('^protos?://(localhost|.+.example.net|.+.example-a.com|.+.example-b.com|.+.example.internal)'); # NOT OK
+
+ /^(example.dev|example.com)/; # OK
+
+ Regexp.new('^http://localhost:8000|' + "^https?://.+.example\\.com/"); # NOT OK
+
+ primary = 'example.com$';
+ Regexp.new('test.' + primary); # NOT OK, but not detected
+
+ Regexp.new('test.' + 'example.com$'); # NOT OK
+
+ Regexp.new('^http://test\.example.com'); # NOT OK
+
+ /^http:\/\/(..|...)\.example\.com\/index\.html/; # OK, wildcards are intentional
+ /^http:\/\/.\.example\.com\/index\.html/; # OK, the wildcard is intentional
+ /^(foo.example\.com|whatever)$/; # kinda OK - one disjunction doesn't even look like a hostname
+end
+def id(e); return e; end
+def convert1(domain)
+ return Regexp.new(domain[:hostname]);
+end
+def convert2(domain)
+ return Regexp.new(domain[:hostname]);
+end
From 097c661362c317eb9bf15a009f045baeee5950bd Mon Sep 17 00:00:00 2001
From: Arthur Baars
Date: Mon, 28 Feb 2022 19:07:28 +0100
Subject: [PATCH 04/13] Ruby: drop results that cannot be found yet from
IncompleteHostnameRegExp.expected
---
.../IncompleteHostnameRegExp.expected | 8 --------
1 file changed, 8 deletions(-)
diff --git a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected
index 944cb284f11..f70cbc19ce7 100644
--- a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected
+++ b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected
@@ -10,23 +10,15 @@
| tst-IncompleteHostnameRegExp.rb:10:3:10:36 | ^http:\\/\\/test.example.com\\/(?:.*) | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:10:2:10:37 | /^http:\\/\\/test.example.com\\/(.../ | here |
| tst-IncompleteHostnameRegExp.rb:11:14:11:37 | ^http://test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:11:13:11:38 | "^http://test.example.com" | here |
| tst-IncompleteHostnameRegExp.rb:12:15:12:38 | ^http://test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:12:14:12:39 | "^http://test.example.com" | here |
-| tst-IncompleteHostnameRegExp.rb:15:23:15:46 | ^http://test.example.com | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:15:13:15:50 | call to id | here |
| tst-IncompleteHostnameRegExp.rb:17:14:17:30 | test.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:17:13:17:31 | `test.example.com$` | here |
| tst-IncompleteHostnameRegExp.rb:19:14:19:30 | ^test.example.com | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:20:13:20:26 | "#{...}$" | here |
| tst-IncompleteHostnameRegExp.rb:20:14:20:31 | ^test.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:20:13:20:26 | "#{...}$" | here |
-| tst-IncompleteHostnameRegExp.rb:22:24:22:40 | test.example.com$ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:23:13:23:29 | ...[...] | here |
-| tst-IncompleteHostnameRegExp.rb:28:24:28:40 | test.example.com$ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:63:20:63:36 | ...[...] | here |
-| tst-IncompleteHostnameRegExp.rb:30:31:30:47 | test.example.com$ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:32:21:32:35 | domain.hostname | here |
| tst-IncompleteHostnameRegExp.rb:37:3:37:53 | ^(https?:)?\\/\\/((service\|www).)?example.com(?=$\|\\/) | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:37:2:37:54 | /^(https?:)?\\/\\/((service\|www).../ | here |
| tst-IncompleteHostnameRegExp.rb:38:3:38:43 | ^(http\|https):\\/\\/www.example.com\\/p\\/f\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:38:2:38:44 | /^(http\|https):\\/\\/www.example.../ | here |
| tst-IncompleteHostnameRegExp.rb:39:5:39:30 | http:\\/\\/sub.example.com\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:39:2:39:33 | /^(http:\\/\\/sub.example.com\\/)/ | here |
| tst-IncompleteHostnameRegExp.rb:40:3:40:29 | ^https?:\\/\\/api.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:40:2:40:30 | /^https?:\\/\\/api.example.com/ | here |
-| tst-IncompleteHostnameRegExp.rb:41:42:41:68 | ^https?://.+\\.example\\.com/ | This string, which is used as a regular expression $@, has an unrestricted wildcard '.+' which may cause 'example\\.com/' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.rb:41:13:41:71 | ... + ... | here |
| tst-IncompleteHostnameRegExp.rb:43:3:43:32 | ^https:\\/\\/[a-z]*.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:43:2:43:33 | /^https:\\/\\/[a-z]*.example.com$/ | here |
| tst-IncompleteHostnameRegExp.rb:44:40:44:53 | .+.example.net | This regular expression has an unescaped '.' before 'example.net', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:44:17:44:109 | "^protos?://(localhost\|.+.exam..." | here |
| tst-IncompleteHostnameRegExp.rb:44:55:44:70 | .+.example-a.com | This regular expression has an unescaped '.' before 'example-a.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:44:17:44:109 | "^protos?://(localhost\|.+.exam..." | here |
| tst-IncompleteHostnameRegExp.rb:44:72:44:87 | .+.example-b.com | This regular expression has an unescaped '.' before 'example-b.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:44:17:44:109 | "^protos?://(localhost\|.+.exam..." | here |
-| tst-IncompleteHostnameRegExp.rb:48:42:48:67 | ^https?://.+.example\\.com/ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example\\.com/', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:48:13:48:69 | ... + ... | here |
-| tst-IncompleteHostnameRegExp.rb:48:42:48:67 | ^https?://.+.example\\.com/ | This string, which is used as a regular expression $@, has an unrestricted wildcard '.+' which may cause 'example\\.com/' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.rb:48:13:48:69 | ... + ... | here |
-| tst-IncompleteHostnameRegExp.rb:53:13:53:36 | test.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:53:13:53:36 | ... + ... | here |
| tst-IncompleteHostnameRegExp.rb:59:5:59:20 | foo.example\\.com | This regular expression has an unescaped '.' before 'example\\.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:59:2:59:32 | /^(foo.example\\.com\|whatever)$/ | here |
From 98f56f4d6015d431b8fe13c407beece7f42ccda7 Mon Sep 17 00:00:00 2001
From: Arthur Baars
Date: Wed, 2 Mar 2022 18:28:47 +0100
Subject: [PATCH 05/13] Js/Ruby: Share IncompleteHostnameRegExp.ql
---
config/identical-files.json | 4 +
.../Security/CWE-020/HostnameRegexpShared.qll | 95 +++++++++++++++++-
.../CWE-020/HostnameRegexpSpecific.qll | 1 +
.../CWE-020/IncompleteHostnameRegExp.ql | 93 +-----------------
.../security/performance/RegExpTreeView.qll | 2 +
.../security/cwe-020/HostnameRegexpShared.qll | 98 ++++++++++++++++++-
.../cwe-020/HostnameRegexpSpecific.qll | 2 +
.../cwe-020/IncompleteHostnameRegExp.ql | 93 +-----------------
8 files changed, 200 insertions(+), 188 deletions(-)
create mode 100644 javascript/ql/src/Security/CWE-020/HostnameRegexpSpecific.qll
create mode 100644 ruby/ql/src/queries/security/cwe-020/HostnameRegexpSpecific.qll
diff --git a/config/identical-files.json b/config/identical-files.json
index bef8b72c87e..a7db98e94eb 100644
--- a/config/identical-files.json
+++ b/config/identical-files.json
@@ -508,5 +508,9 @@
"java/ql/lib/semmle/code/java/dataflow/internal/AccessPathSyntax.qll",
"javascript/ql/lib/semmle/javascript/frameworks/data/internal/AccessPathSyntax.qll",
"ruby/ql/lib/codeql/ruby/dataflow/internal/AccessPathSyntax.qll"
+ ],
+ "Hostname Regexp queries": [
+ "javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll",
+ "ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll"
]
}
diff --git a/javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll b/javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll
index 97d75d4a524..d003d4b9a10 100644
--- a/javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll
+++ b/javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll
@@ -3,7 +3,7 @@
* that match URLs and hostname patterns.
*/
-import javascript
+private import HostnameRegexpSpecific
/**
* Holds if the given constant is unlikely to occur in the origin part of a URL.
@@ -107,3 +107,96 @@ predicate alwaysMatchesHostnameAlt(RegExpAlt alt, int i) {
alwaysMatchesHostnameAlt(alt, i - 1) and
alwaysMatchesHostname(alt.getChild(i))
}
+
+/**
+ * Holds if `term` occurs inside a quantifier or alternative (and thus
+ * can not be expected to correspond to a unique match), or as part of
+ * a lookaround assertion (which are rarely used for capture groups).
+ */
+predicate isInsideChoiceOrSubPattern(RegExpTerm term) {
+ exists(RegExpParent parent | parent = term.getParent() |
+ parent instanceof RegExpAlt
+ or
+ parent instanceof RegExpQuantifier
+ or
+ parent instanceof RegExpSubPattern
+ or
+ isInsideChoiceOrSubPattern(parent)
+ )
+}
+
+/**
+ * Holds if `group` is likely to be used as a capture group.
+ */
+predicate isLikelyCaptureGroup(RegExpGroup group) {
+ group.isCapture() and
+ not isInsideChoiceOrSubPattern(group)
+}
+
+/**
+ * Holds if `seq` contains two consecutive dots `..` or escaped dots.
+ *
+ * At least one of these dots is not intended to be a subdomain separator,
+ * so we avoid flagging the pattern in this case.
+ */
+predicate hasConsecutiveDots(RegExpSequence seq) {
+ exists(int i |
+ isDotLike(seq.getChild(i)) and
+ isDotLike(seq.getChild(i + 1))
+ )
+}
+
+predicate isIncompleteHostNameRegExpPattern(RegExpTerm regexp, RegExpSequence seq, string msg) {
+ seq = regexp.getAChild*() and
+ exists(RegExpDot unescapedDot, int i, string hostname |
+ hasTopLevelDomainEnding(seq, i) and
+ not isConstantInvalidInsideOrigin(seq.getChild([0 .. i - 1]).getAChild*()) and
+ not isLikelyCaptureGroup(seq.getChild([i .. seq.getNumChild() - 1]).getAChild*()) and
+ unescapedDot = seq.getChild([0 .. i - 1]).getAChild*() and
+ unescapedDot != seq.getChild(i - 1) and // Should not be the '.' immediately before the TLD
+ not hasConsecutiveDots(unescapedDot.getParent()) and
+ hostname =
+ seq.getChild(i - 2).getRawValue() + seq.getChild(i - 1).getRawValue() +
+ seq.getChild(i).getRawValue()
+ |
+ if unescapedDot.getParent() instanceof RegExpQuantifier
+ then
+ // `.*\.example.com` can match `evil.com/?x=.example.com`
+ //
+ // This problem only occurs when the pattern is applied against a full URL, not just a hostname/origin.
+ // We therefore check if the pattern includes a suffix after the TLD, such as `.*\.example.com/`.
+ // Note that a post-anchored pattern (`.*\.example.com$`) will usually fail to match a full URL,
+ // and patterns with neither a suffix nor an anchor fall under the purview of MissingRegExpAnchor.
+ seq.getChild(0) instanceof RegExpCaret and
+ not seq.getAChild() instanceof RegExpDollar and
+ seq.getChild([i .. i + 1]).(RegExpConstant).getValue().regexpMatch(".*[/?#].*") and
+ msg =
+ "has an unrestricted wildcard '" + unescapedDot.getParent().(RegExpQuantifier).getRawValue()
+ + "' which may cause '" + hostname +
+ "' to be matched anywhere in the URL, outside the hostname."
+ else
+ msg =
+ "has an unescaped '.' before '" + hostname +
+ "', so it might match more hosts than expected."
+ )
+}
+
+predicate incompleteHostnameRegExp(
+ RegExpSequence hostSequence, string message, DataFlow::Node aux, string label
+) {
+ exists(RegExpPatternSource re, RegExpTerm regexp, string msg, string kind |
+ regexp = re.getRegExpTerm() and
+ isIncompleteHostNameRegExpPattern(regexp, hostSequence, msg) and
+ (
+ if re.getAParse() != re
+ then (
+ kind = "string, which is used as a regular expression $@," and
+ aux = re.getAParse()
+ ) else (
+ kind = "regular expression" and aux = re
+ )
+ )
+ |
+ message = "This " + kind + " " + msg and label = "here"
+ )
+}
diff --git a/javascript/ql/src/Security/CWE-020/HostnameRegexpSpecific.qll b/javascript/ql/src/Security/CWE-020/HostnameRegexpSpecific.qll
new file mode 100644
index 00000000000..7046b14f09d
--- /dev/null
+++ b/javascript/ql/src/Security/CWE-020/HostnameRegexpSpecific.qll
@@ -0,0 +1 @@
+import javascript
diff --git a/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.ql b/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.ql
index bd095887011..3ae844bf57f 100644
--- a/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.ql
+++ b/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.ql
@@ -11,97 +11,6 @@
* external/cwe/cwe-020
*/
-import javascript
-import semmle.javascript.CharacterEscapes
import HostnameRegexpShared
-/**
- * Holds if `term` occurs inside a quantifier or alternative (and thus
- * can not be expected to correspond to a unique match), or as part of
- * a lookaround assertion (which are rarely used for capture groups).
- */
-predicate isInsideChoiceOrSubPattern(RegExpTerm term) {
- exists(RegExpParent parent | parent = term.getParent() |
- parent instanceof RegExpAlt
- or
- parent instanceof RegExpQuantifier
- or
- parent instanceof RegExpSubPattern
- or
- isInsideChoiceOrSubPattern(parent)
- )
-}
-
-/**
- * Holds if `group` is likely to be used as a capture group.
- */
-predicate isLikelyCaptureGroup(RegExpGroup group) {
- group.isCapture() and
- not isInsideChoiceOrSubPattern(group)
-}
-
-/**
- * Holds if `seq` contains two consecutive dots `..` or escaped dots.
- *
- * At least one of these dots is not intended to be a subdomain separator,
- * so we avoid flagging the pattern in this case.
- */
-predicate hasConsecutiveDots(RegExpSequence seq) {
- exists(int i |
- isDotLike(seq.getChild(i)) and
- isDotLike(seq.getChild(i + 1))
- )
-}
-
-predicate isIncompleteHostNameRegExpPattern(RegExpTerm regexp, RegExpSequence seq, string msg) {
- seq = regexp.getAChild*() and
- exists(RegExpDot unescapedDot, int i, string hostname |
- hasTopLevelDomainEnding(seq, i) and
- not isConstantInvalidInsideOrigin(seq.getChild([0 .. i - 1]).getAChild*()) and
- not isLikelyCaptureGroup(seq.getChild([i .. seq.getNumChild() - 1]).getAChild*()) and
- unescapedDot = seq.getChild([0 .. i - 1]).getAChild*() and
- unescapedDot != seq.getChild(i - 1) and // Should not be the '.' immediately before the TLD
- not hasConsecutiveDots(unescapedDot.getParent()) and
- hostname =
- seq.getChild(i - 2).getRawValue() + seq.getChild(i - 1).getRawValue() +
- seq.getChild(i).getRawValue()
- |
- if unescapedDot.getParent() instanceof RegExpQuantifier
- then
- // `.*\.example.com` can match `evil.com/?x=.example.com`
- //
- // This problem only occurs when the pattern is applied against a full URL, not just a hostname/origin.
- // We therefore check if the pattern includes a suffix after the TLD, such as `.*\.example.com/`.
- // Note that a post-anchored pattern (`.*\.example.com$`) will usually fail to match a full URL,
- // and patterns with neither a suffix nor an anchor fall under the purview of MissingRegExpAnchor.
- seq.getChild(0) instanceof RegExpCaret and
- not seq.getAChild() instanceof RegExpDollar and
- seq.getChild([i .. i + 1]).(RegExpConstant).getValue().regexpMatch(".*[/?#].*") and
- msg =
- "has an unrestricted wildcard '" + unescapedDot.getParent().(RegExpQuantifier).getRawValue()
- + "' which may cause '" + hostname +
- "' to be matched anywhere in the URL, outside the hostname."
- else
- msg =
- "has an unescaped '.' before '" + hostname +
- "', so it might match more hosts than expected."
- )
-}
-
-from
- RegExpPatternSource re, RegExpTerm regexp, RegExpSequence hostSequence, string msg, string kind,
- DataFlow::Node aux
-where
- regexp = re.getRegExpTerm() and
- isIncompleteHostNameRegExpPattern(regexp, hostSequence, msg) and
- (
- if re.getAParse() != re
- then (
- kind = "string, which is used as a regular expression $@," and
- aux = re.getAParse()
- ) else (
- kind = "regular expression" and aux = re
- )
- ) and
- not CharacterEscapes::hasALikelyRegExpPatternMistake(re)
-select hostSequence, "This " + kind + " " + msg, aux, "here"
+query predicate problems = incompleteHostnameRegExp/4;
diff --git a/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll b/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll
index 9043fa949ba..7b5017239a6 100644
--- a/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll
+++ b/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll
@@ -398,6 +398,8 @@ class RegExpAlt extends RegExpTerm, TRegExpAlt {
override string getAPrimaryQlClass() { result = "RegExpAlt" }
}
+class RegExpCharEscape = RegExpEscape;
+
class RegExpEscape extends RegExpNormalChar {
RegExpEscape() { re.escapedCharacter(start, end) }
diff --git a/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll b/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll
index 102d719979e..d003d4b9a10 100644
--- a/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll
+++ b/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll
@@ -3,8 +3,7 @@
* that match URLs and hostname patterns.
*/
-private import codeql.ruby.security.performance.RegExpTreeView
-private import codeql.ruby.DataFlow
+private import HostnameRegexpSpecific
/**
* Holds if the given constant is unlikely to occur in the origin part of a URL.
@@ -20,7 +19,7 @@ predicate isConstantInvalidInsideOrigin(RegExpConstant term) {
/** Holds if `term` is a dot constant of form `\.` or `[.]`. */
predicate isDotConstant(RegExpTerm term) {
- term.(RegExpEscape).getValue() = "."
+ term.(RegExpCharEscape).getValue() = "."
or
exists(RegExpCharacterClass cls |
term = cls and
@@ -108,3 +107,96 @@ predicate alwaysMatchesHostnameAlt(RegExpAlt alt, int i) {
alwaysMatchesHostnameAlt(alt, i - 1) and
alwaysMatchesHostname(alt.getChild(i))
}
+
+/**
+ * Holds if `term` occurs inside a quantifier or alternative (and thus
+ * can not be expected to correspond to a unique match), or as part of
+ * a lookaround assertion (which are rarely used for capture groups).
+ */
+predicate isInsideChoiceOrSubPattern(RegExpTerm term) {
+ exists(RegExpParent parent | parent = term.getParent() |
+ parent instanceof RegExpAlt
+ or
+ parent instanceof RegExpQuantifier
+ or
+ parent instanceof RegExpSubPattern
+ or
+ isInsideChoiceOrSubPattern(parent)
+ )
+}
+
+/**
+ * Holds if `group` is likely to be used as a capture group.
+ */
+predicate isLikelyCaptureGroup(RegExpGroup group) {
+ group.isCapture() and
+ not isInsideChoiceOrSubPattern(group)
+}
+
+/**
+ * Holds if `seq` contains two consecutive dots `..` or escaped dots.
+ *
+ * At least one of these dots is not intended to be a subdomain separator,
+ * so we avoid flagging the pattern in this case.
+ */
+predicate hasConsecutiveDots(RegExpSequence seq) {
+ exists(int i |
+ isDotLike(seq.getChild(i)) and
+ isDotLike(seq.getChild(i + 1))
+ )
+}
+
+predicate isIncompleteHostNameRegExpPattern(RegExpTerm regexp, RegExpSequence seq, string msg) {
+ seq = regexp.getAChild*() and
+ exists(RegExpDot unescapedDot, int i, string hostname |
+ hasTopLevelDomainEnding(seq, i) and
+ not isConstantInvalidInsideOrigin(seq.getChild([0 .. i - 1]).getAChild*()) and
+ not isLikelyCaptureGroup(seq.getChild([i .. seq.getNumChild() - 1]).getAChild*()) and
+ unescapedDot = seq.getChild([0 .. i - 1]).getAChild*() and
+ unescapedDot != seq.getChild(i - 1) and // Should not be the '.' immediately before the TLD
+ not hasConsecutiveDots(unescapedDot.getParent()) and
+ hostname =
+ seq.getChild(i - 2).getRawValue() + seq.getChild(i - 1).getRawValue() +
+ seq.getChild(i).getRawValue()
+ |
+ if unescapedDot.getParent() instanceof RegExpQuantifier
+ then
+ // `.*\.example.com` can match `evil.com/?x=.example.com`
+ //
+ // This problem only occurs when the pattern is applied against a full URL, not just a hostname/origin.
+ // We therefore check if the pattern includes a suffix after the TLD, such as `.*\.example.com/`.
+ // Note that a post-anchored pattern (`.*\.example.com$`) will usually fail to match a full URL,
+ // and patterns with neither a suffix nor an anchor fall under the purview of MissingRegExpAnchor.
+ seq.getChild(0) instanceof RegExpCaret and
+ not seq.getAChild() instanceof RegExpDollar and
+ seq.getChild([i .. i + 1]).(RegExpConstant).getValue().regexpMatch(".*[/?#].*") and
+ msg =
+ "has an unrestricted wildcard '" + unescapedDot.getParent().(RegExpQuantifier).getRawValue()
+ + "' which may cause '" + hostname +
+ "' to be matched anywhere in the URL, outside the hostname."
+ else
+ msg =
+ "has an unescaped '.' before '" + hostname +
+ "', so it might match more hosts than expected."
+ )
+}
+
+predicate incompleteHostnameRegExp(
+ RegExpSequence hostSequence, string message, DataFlow::Node aux, string label
+) {
+ exists(RegExpPatternSource re, RegExpTerm regexp, string msg, string kind |
+ regexp = re.getRegExpTerm() and
+ isIncompleteHostNameRegExpPattern(regexp, hostSequence, msg) and
+ (
+ if re.getAParse() != re
+ then (
+ kind = "string, which is used as a regular expression $@," and
+ aux = re.getAParse()
+ ) else (
+ kind = "regular expression" and aux = re
+ )
+ )
+ |
+ message = "This " + kind + " " + msg and label = "here"
+ )
+}
diff --git a/ruby/ql/src/queries/security/cwe-020/HostnameRegexpSpecific.qll b/ruby/ql/src/queries/security/cwe-020/HostnameRegexpSpecific.qll
new file mode 100644
index 00000000000..d01dae07b8b
--- /dev/null
+++ b/ruby/ql/src/queries/security/cwe-020/HostnameRegexpSpecific.qll
@@ -0,0 +1,2 @@
+import codeql.ruby.security.performance.RegExpTreeView
+import codeql.ruby.DataFlow
diff --git a/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.ql b/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.ql
index 5526978b07a..f300fc9403f 100644
--- a/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.ql
+++ b/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.ql
@@ -12,96 +12,5 @@
*/
import HostnameRegexpShared
-import codeql.ruby.security.performance.RegExpTreeView
-import codeql.ruby.DataFlow
-import codeql.ruby.AST as AST
-/**
- * Holds if `term` occurs inside a quantifier or alternative (and thus
- * can not be expected to correspond to a unique match), or as part of
- * a lookaround assertion (which are rarely used for capture groups).
- */
-predicate isInsideChoiceOrSubPattern(RegExpTerm term) {
- exists(RegExpParent parent | parent = term.getParent() |
- parent instanceof RegExpAlt
- or
- parent instanceof RegExpQuantifier
- or
- parent instanceof RegExpSubPattern
- or
- isInsideChoiceOrSubPattern(parent)
- )
-}
-
-/**
- * Holds if `group` is likely to be used as a capture group.
- */
-predicate isLikelyCaptureGroup(RegExpGroup group) {
- group.isCapture() and
- not isInsideChoiceOrSubPattern(group)
-}
-
-/**
- * Holds if `seq` contains two consecutive dots `..` or escaped dots.
- *
- * At least one of these dots is not intended to be a subdomain separator,
- * so we avoid flagging the pattern in this case.
- */
-predicate hasConsecutiveDots(RegExpSequence seq) {
- exists(int i |
- isDotLike(seq.getChild(i)) and
- isDotLike(seq.getChild(i + 1))
- )
-}
-
-predicate isIncompleteHostNameRegExpPattern(RegExpTerm regexp, RegExpSequence seq, string msg) {
- seq = regexp.getAChild*() and
- exists(RegExpDot unescapedDot, int i, string hostname |
- hasTopLevelDomainEnding(seq, i) and
- not isConstantInvalidInsideOrigin(seq.getChild([0 .. i - 1]).getAChild*()) and
- not isLikelyCaptureGroup(seq.getChild([i .. seq.getNumChild() - 1]).getAChild*()) and
- unescapedDot = seq.getChild([0 .. i - 1]).getAChild*() and
- unescapedDot != seq.getChild(i - 1) and // Should not be the '.' immediately before the TLD
- not hasConsecutiveDots(unescapedDot.getParent()) and
- hostname =
- seq.getChild(i - 2).getRawValue() + seq.getChild(i - 1).getRawValue() +
- seq.getChild(i).getRawValue()
- |
- if unescapedDot.getParent() instanceof RegExpQuantifier
- then
- // `.*\.example.com` can match `evil.com/?x=.example.com`
- //
- // This problem only occurs when the pattern is applied against a full URL, not just a hostname/origin.
- // We therefore check if the pattern includes a suffix after the TLD, such as `.*\.example.com/`.
- // Note that a post-anchored pattern (`.*\.example.com$`) will usually fail to match a full URL,
- // and patterns with neither a suffix nor an anchor fall under the purview of MissingRegExpAnchor.
- seq.getChild(0) instanceof RegExpCaret and
- not seq.getAChild() instanceof RegExpDollar and
- seq.getChild([i .. i + 1]).(RegExpConstant).getValue().regexpMatch(".*[/?#].*") and
- msg =
- "has an unrestricted wildcard '" + unescapedDot.getParent().(RegExpQuantifier).getRawValue()
- + "' which may cause '" + hostname +
- "' to be matched anywhere in the URL, outside the hostname."
- else
- msg =
- "has an unescaped '.' before '" + hostname +
- "', so it might match more hosts than expected."
- )
-}
-
-from
- RegExpPatternSource re, RegExpTerm regexp, RegExpSequence hostSequence, string msg, string kind,
- DataFlow::Node aux
-where
- regexp = re.getRegExpTerm() and
- isIncompleteHostNameRegExpPattern(regexp, hostSequence, msg) and
- (
- if re.getAParse() != re
- then (
- kind = "string, which is used as a regular expression $@," and
- aux = re.getAParse()
- ) else (
- kind = "regular expression" and aux = re
- )
- )
-select hostSequence, "This " + kind + " " + msg, aux, "here"
+query predicate problems = incompleteHostnameRegExp/4;
From bb348116ab515c9a218f27603829ba79420d7137 Mon Sep 17 00:00:00 2001
From: Arthur Baars
Date: Fri, 4 Mar 2022 13:48:24 +0100
Subject: [PATCH 06/13] JavaScript: update expected output
---
.../Security/CWE-020/IncompleteHostnameRegExp.expected | 1 +
.../Security/CWE-020/tst-IncompleteHostnameRegExp.js | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/javascript/ql/test/query-tests/Security/CWE-020/IncompleteHostnameRegExp.expected b/javascript/ql/test/query-tests/Security/CWE-020/IncompleteHostnameRegExp.expected
index 13950be73ba..d0fb91322fe 100644
--- a/javascript/ql/test/query-tests/Security/CWE-020/IncompleteHostnameRegExp.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-020/IncompleteHostnameRegExp.expected
@@ -23,4 +23,5 @@
| tst-IncompleteHostnameRegExp.js:48:42:48:47 | ^https?://.+.example\\.com/ | This regular expression has an unescaped '.' before 'example\\.com/', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:48:13:48:69 | '^http: ... \\.com/' | here |
| tst-IncompleteHostnameRegExp.js:48:42:48:47 | ^https?://.+.example\\.com/ | This regular expression has an unrestricted wildcard '.+' which may cause 'example\\.com/' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.js:48:13:48:69 | '^http: ... \\.com/' | here |
| tst-IncompleteHostnameRegExp.js:53:14:53:35 | test.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:53:13:53:36 | 'test.' ... e.com$' | here |
+| tst-IncompleteHostnameRegExp.js:55:14:55:38 | ^http://test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:55:13:55:39 | '^http: ... le.com' | here |
| tst-IncompleteHostnameRegExp.js:59:5:59:20 | foo.example\\.com | This regular expression has an unescaped '.' before 'example\\.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:59:2:59:32 | /^(foo. ... ever)$/ | here |
diff --git a/javascript/ql/test/query-tests/Security/CWE-020/tst-IncompleteHostnameRegExp.js b/javascript/ql/test/query-tests/Security/CWE-020/tst-IncompleteHostnameRegExp.js
index df887092d23..ddc267ebdd7 100644
--- a/javascript/ql/test/query-tests/Security/CWE-020/tst-IncompleteHostnameRegExp.js
+++ b/javascript/ql/test/query-tests/Security/CWE-020/tst-IncompleteHostnameRegExp.js
@@ -52,7 +52,7 @@
new RegExp('test.' + 'example.com$'); // NOT OK
- new RegExp('^http://test\.example.com'); // NOT OK, but flagged by js/useless-regexp-character-escape
+ new RegExp('^http://test\.example.com'); // NOT OK
/^http:\/\/(..|...)\.example\.com\/index\.html/; // OK, wildcards are intentional
/^http:\/\/.\.example\.com\/index\.html/; // OK, the wildcard is intentional
From ce50f35ddaa0598272624032cd72aed8ae539b7f Mon Sep 17 00:00:00 2001
From: Arthur Baars
Date: Thu, 3 Mar 2022 17:34:53 +0100
Subject: [PATCH 07/13] Python: switch to shared implementation of
IncompleteHostnameRegExp.ql
---
config/identical-files.json | 3 +-
python/ql/lib/semmle/python/RegexTreeView.qll | 40 ++++
.../Security/CWE-020/HostnameRegexpShared.qll | 202 ++++++++++++++++++
.../CWE-020/HostnameRegexpSpecific.qll | 2 +
.../CWE-020/IncompleteHostnameRegExp.ql | 32 +--
.../IncompleteHostnameRegExp.expected | 2 +-
6 files changed, 250 insertions(+), 31 deletions(-)
create mode 100644 python/ql/src/Security/CWE-020/HostnameRegexpShared.qll
create mode 100644 python/ql/src/Security/CWE-020/HostnameRegexpSpecific.qll
diff --git a/config/identical-files.json b/config/identical-files.json
index a7db98e94eb..daf764f15c6 100644
--- a/config/identical-files.json
+++ b/config/identical-files.json
@@ -511,6 +511,7 @@
],
"Hostname Regexp queries": [
"javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll",
- "ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll"
+ "ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll",
+ "python/ql/src/Security/CWE-020/HostnameRegexpShared.qll"
]
}
diff --git a/python/ql/lib/semmle/python/RegexTreeView.qll b/python/ql/lib/semmle/python/RegexTreeView.qll
index 95d983f5e88..37d6d307ef4 100644
--- a/python/ql/lib/semmle/python/RegexTreeView.qll
+++ b/python/ql/lib/semmle/python/RegexTreeView.qll
@@ -2,6 +2,7 @@
import python
private import semmle.python.regex
+private import semmle.python.dataflow.new.DataFlow
/**
* An element containing a regular expression term, that is, either
@@ -48,6 +49,19 @@ newtype TRegExpParent =
/** A back reference */
TRegExpBackRef(Regex re, int start, int end) { re.backreference(start, end) }
+/**
+ * Provides regular expression patterns.
+ */
+module RegExpPatterns {
+ /**
+ * Gets a pattern that matches common top-level domain names in lower case.
+ */
+ string commonTLD() {
+ // according to ranking by http://google.com/search?q=site:.<>
+ result = "(?:com|org|edu|gov|uk|net|io)(?![a-z0-9])"
+ }
+}
+
/**
* An element containing a regular expression term, that is, either
* a string literal (parsed as a regular expression)
@@ -445,6 +459,8 @@ class RegExpAlt extends RegExpTerm, TRegExpAlt {
override string getPrimaryQLClass() { result = "RegExpAlt" }
}
+class RegExpCharEscape = RegExpEscape;
+
/**
* An escaped regular expression term, that is, a regular expression
* term starting with a backslash, which is not a backreference.
@@ -751,6 +767,9 @@ class RegExpGroup extends RegExpTerm, TRegExpGroup {
*/
int getNumber() { result = re.getGroupNumber(start, end) }
+ /** Holds if this is a capture group. */
+ predicate isCapture() { not exists(this.getNumber()) }
+
/** Holds if this is a named capture group. */
predicate isNamed() { exists(this.getName()) }
@@ -1009,3 +1028,24 @@ class RegExpBackRef extends RegExpTerm, TRegExpBackRef {
/** Gets the parse tree resulting from parsing `re`, if such has been constructed. */
RegExpTerm getParsedRegExp(StrConst re) { result.getRegex() = re and result.isRootTerm() }
+
+/**
+ * A node whose value may flow to a position where it is interpreted
+ * as a part of a regular expression.
+ */
+class RegExpPatternSource extends DataFlow::CfgNode {
+ private Regex astNode;
+
+ RegExpPatternSource() { astNode = this.asExpr() }
+
+ /**
+ * Gets a node where the pattern of this node is parsed as a part of
+ * a regular expression.
+ */
+ DataFlow::Node getAParse() { result = this }
+
+ /**
+ * Gets the root term of the regular expression parsed from this pattern.
+ */
+ RegExpTerm getRegExpTerm() { result.getRegex() = astNode }
+}
diff --git a/python/ql/src/Security/CWE-020/HostnameRegexpShared.qll b/python/ql/src/Security/CWE-020/HostnameRegexpShared.qll
new file mode 100644
index 00000000000..d003d4b9a10
--- /dev/null
+++ b/python/ql/src/Security/CWE-020/HostnameRegexpShared.qll
@@ -0,0 +1,202 @@
+/**
+ * Provides predicates for reasoning about regular expressions
+ * that match URLs and hostname patterns.
+ */
+
+private import HostnameRegexpSpecific
+
+/**
+ * Holds if the given constant is unlikely to occur in the origin part of a URL.
+ */
+predicate isConstantInvalidInsideOrigin(RegExpConstant term) {
+ // Look for any of these cases:
+ // - A character that can't occur in the origin
+ // - Two dashes in a row
+ // - A colon that is not part of port or scheme separator
+ // - A slash that is not part of scheme separator
+ term.getValue().regexpMatch(".*(?:[^a-zA-Z0-9.:/-]|--|:[^0-9/]|(?
Date: Fri, 11 Mar 2022 14:25:34 +0100
Subject: [PATCH 08/13] Address comments
---
javascript/ql/lib/semmle/javascript/Regexp.qll | 4 ++--
javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll | 2 +-
.../Security/CWE-020/IncompleteUrlSubstringSanitization.ql | 2 +-
python/ql/lib/semmle/python/RegexTreeView.qll | 6 +++---
python/ql/src/Security/CWE-020/HostnameRegexpShared.qll | 2 +-
.../lib/codeql/ruby/security/performance/RegExpTreeView.qll | 6 +++---
.../src/queries/security/cwe-020/HostnameRegexpShared.qll | 2 +-
7 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/javascript/ql/lib/semmle/javascript/Regexp.qll b/javascript/ql/lib/semmle/javascript/Regexp.qll
index ddb50436fb8..33bc76d9dca 100644
--- a/javascript/ql/lib/semmle/javascript/Regexp.qll
+++ b/javascript/ql/lib/semmle/javascript/Regexp.qll
@@ -999,13 +999,13 @@ predicate isInterpretedAsRegExp(DataFlow::Node source) {
}
/**
- * Provides regular expression patterns.
+ * Provides utility predicates related to regular expressions.
*/
module RegExpPatterns {
/**
* Gets a pattern that matches common top-level domain names in lower case.
*/
- string commonTLD() {
+ string getACommonTld() {
// according to ranking by http://google.com/search?q=site:.<>
result = "(?:com|org|edu|gov|uk|net|io)(?![a-z0-9])"
}
diff --git a/javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll b/javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll
index d003d4b9a10..2192951f76d 100644
--- a/javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll
+++ b/javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll
@@ -62,7 +62,7 @@ predicate hasTopLevelDomainEnding(RegExpSequence seq, int i) {
seq.getChild(i)
.(RegExpConstant)
.getValue()
- .regexpMatch("(?i)" + RegExpPatterns::commonTLD() + "(:\\d+)?([/?#].*)?") and
+ .regexpMatch("(?i)" + RegExpPatterns::getACommonTld() + "(:\\d+)?([/?#].*)?") and
isDotLike(seq.getChild(i - 1)) and
not (i = 1 and matchesBeginningOfString(seq))
}
diff --git a/javascript/ql/src/Security/CWE-020/IncompleteUrlSubstringSanitization.ql b/javascript/ql/src/Security/CWE-020/IncompleteUrlSubstringSanitization.ql
index 56ab631d1e1..466022784f4 100644
--- a/javascript/ql/src/Security/CWE-020/IncompleteUrlSubstringSanitization.ql
+++ b/javascript/ql/src/Security/CWE-020/IncompleteUrlSubstringSanitization.ql
@@ -39,7 +39,7 @@ where
(
// target contains a domain on a common TLD, and perhaps some other URL components
target
- .regexpMatch("(?i)([a-z]*:?//)?\\.?([a-z0-9-]+\\.)+" + RegExpPatterns::commonTLD() +
+ .regexpMatch("(?i)([a-z]*:?//)?\\.?([a-z0-9-]+\\.)+" + RegExpPatterns::getACommonTld() +
"(:[0-9]+)?/?")
or
// target is a HTTP URL to a domain on any TLD
diff --git a/python/ql/lib/semmle/python/RegexTreeView.qll b/python/ql/lib/semmle/python/RegexTreeView.qll
index 37d6d307ef4..019089ecbc2 100644
--- a/python/ql/lib/semmle/python/RegexTreeView.qll
+++ b/python/ql/lib/semmle/python/RegexTreeView.qll
@@ -50,13 +50,13 @@ newtype TRegExpParent =
TRegExpBackRef(Regex re, int start, int end) { re.backreference(start, end) }
/**
- * Provides regular expression patterns.
+ * Provides utility predicates related to regular expressions.
*/
module RegExpPatterns {
/**
* Gets a pattern that matches common top-level domain names in lower case.
*/
- string commonTLD() {
+ string getACommonTld() {
// according to ranking by http://google.com/search?q=site:.<>
result = "(?:com|org|edu|gov|uk|net|io)(?![a-z0-9])"
}
@@ -768,7 +768,7 @@ class RegExpGroup extends RegExpTerm, TRegExpGroup {
int getNumber() { result = re.getGroupNumber(start, end) }
/** Holds if this is a capture group. */
- predicate isCapture() { not exists(this.getNumber()) }
+ predicate isCapture() { exists(this.getNumber()) }
/** Holds if this is a named capture group. */
predicate isNamed() { exists(this.getName()) }
diff --git a/python/ql/src/Security/CWE-020/HostnameRegexpShared.qll b/python/ql/src/Security/CWE-020/HostnameRegexpShared.qll
index d003d4b9a10..2192951f76d 100644
--- a/python/ql/src/Security/CWE-020/HostnameRegexpShared.qll
+++ b/python/ql/src/Security/CWE-020/HostnameRegexpShared.qll
@@ -62,7 +62,7 @@ predicate hasTopLevelDomainEnding(RegExpSequence seq, int i) {
seq.getChild(i)
.(RegExpConstant)
.getValue()
- .regexpMatch("(?i)" + RegExpPatterns::commonTLD() + "(:\\d+)?([/?#].*)?") and
+ .regexpMatch("(?i)" + RegExpPatterns::getACommonTld() + "(:\\d+)?([/?#].*)?") and
isDotLike(seq.getChild(i - 1)) and
not (i = 1 and matchesBeginningOfString(seq))
}
diff --git a/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll b/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll
index 7b5017239a6..c059f7f131c 100644
--- a/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll
+++ b/ruby/ql/lib/codeql/ruby/security/performance/RegExpTreeView.qll
@@ -60,13 +60,13 @@ module RegExpFlags {
}
/**
- * Provides regular expression patterns.
+ * Provides utility predicates related to regular expressions.
*/
module RegExpPatterns {
/**
* Gets a pattern that matches common top-level domain names in lower case.
*/
- string commonTLD() {
+ string getACommonTld() {
// according to ranking by http://google.com/search?q=site:.<>
result = "(?:com|org|edu|gov|uk|net|io)(?![a-z0-9])"
}
@@ -640,7 +640,7 @@ class RegExpGroup extends RegExpTerm, TRegExpGroup {
int getNumber() { result = re.getGroupNumber(start, end) }
/** Holds if this is a capture group. */
- predicate isCapture() { not exists(this.getNumber()) }
+ predicate isCapture() { exists(this.getNumber()) }
/** Holds if this is a named capture group. */
predicate isNamed() { exists(this.getName()) }
diff --git a/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll b/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll
index d003d4b9a10..2192951f76d 100644
--- a/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll
+++ b/ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll
@@ -62,7 +62,7 @@ predicate hasTopLevelDomainEnding(RegExpSequence seq, int i) {
seq.getChild(i)
.(RegExpConstant)
.getValue()
- .regexpMatch("(?i)" + RegExpPatterns::commonTLD() + "(:\\d+)?([/?#].*)?") and
+ .regexpMatch("(?i)" + RegExpPatterns::getACommonTld() + "(:\\d+)?([/?#].*)?") and
isDotLike(seq.getChild(i - 1)) and
not (i = 1 and matchesBeginningOfString(seq))
}
From 852f05bfb7aa32edf70a753d0d2b90d5e228b34c Mon Sep 17 00:00:00 2001
From: Arthur Baars
Date: Wed, 16 Mar 2022 12:26:39 +0100
Subject: [PATCH 09/13] Address comment
---
javascript/ql/lib/semmle/javascript/Regexp.qll | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/javascript/ql/lib/semmle/javascript/Regexp.qll b/javascript/ql/lib/semmle/javascript/Regexp.qll
index 33bc76d9dca..a208a98753f 100644
--- a/javascript/ql/lib/semmle/javascript/Regexp.qll
+++ b/javascript/ql/lib/semmle/javascript/Regexp.qll
@@ -1009,6 +1009,12 @@ module RegExpPatterns {
// according to ranking by http://google.com/search?q=site:.<>
result = "(?:com|org|edu|gov|uk|net|io)(?![a-z0-9])"
}
+
+ /**
+ * Gets a pattern that matches common top-level domain names in lower case.
+ * DEPRECATED: use `getACommonTld` instead
+ */
+ deprecated predicate commonTLD = getACommonTld/0;
}
/**
From 6b323eeda86970a08fbd53ab2167aa82868beed5 Mon Sep 17 00:00:00 2001
From: Arthur Baars
Date: Wed, 16 Mar 2022 12:34:03 +0100
Subject: [PATCH 10/13] Update expected output
---
.../IncompleteHostnameRegExp.expected | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected
index f70cbc19ce7..8ab11cff1d6 100644
--- a/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected
+++ b/ruby/ql/test/query-tests/security/cwe-020/IncompleteHostnameRegExp/IncompleteHostnameRegExp.expected
@@ -10,6 +10,7 @@
| tst-IncompleteHostnameRegExp.rb:10:3:10:36 | ^http:\\/\\/test.example.com\\/(?:.*) | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:10:2:10:37 | /^http:\\/\\/test.example.com\\/(.../ | here |
| tst-IncompleteHostnameRegExp.rb:11:14:11:37 | ^http://test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:11:13:11:38 | "^http://test.example.com" | here |
| tst-IncompleteHostnameRegExp.rb:12:15:12:38 | ^http://test.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:12:14:12:39 | "^http://test.example.com" | here |
+| tst-IncompleteHostnameRegExp.rb:15:23:15:46 | ^http://test.example.com | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:15:13:15:50 | call to id | here |
| tst-IncompleteHostnameRegExp.rb:17:14:17:30 | test.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:17:13:17:31 | `test.example.com$` | here |
| tst-IncompleteHostnameRegExp.rb:19:14:19:30 | ^test.example.com | This string, which is used as a regular expression $@, has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:20:13:20:26 | "#{...}$" | here |
| tst-IncompleteHostnameRegExp.rb:20:14:20:31 | ^test.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:20:13:20:26 | "#{...}$" | here |
@@ -17,8 +18,11 @@
| tst-IncompleteHostnameRegExp.rb:38:3:38:43 | ^(http\|https):\\/\\/www.example.com\\/p\\/f\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:38:2:38:44 | /^(http\|https):\\/\\/www.example.../ | here |
| tst-IncompleteHostnameRegExp.rb:39:5:39:30 | http:\\/\\/sub.example.com\\/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:39:2:39:33 | /^(http:\\/\\/sub.example.com\\/)/ | here |
| tst-IncompleteHostnameRegExp.rb:40:3:40:29 | ^https?:\\/\\/api.example.com | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:40:2:40:30 | /^https?:\\/\\/api.example.com/ | here |
+| tst-IncompleteHostnameRegExp.rb:41:42:41:68 | ^https?://.+\\.example\\.com/ | This string, which is used as a regular expression $@, has an unrestricted wildcard '.+' which may cause 'example\\.com/' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.rb:41:13:41:71 | ... + ... | here |
| tst-IncompleteHostnameRegExp.rb:43:3:43:32 | ^https:\\/\\/[a-z]*.example.com$ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:43:2:43:33 | /^https:\\/\\/[a-z]*.example.com$/ | here |
| tst-IncompleteHostnameRegExp.rb:44:40:44:53 | .+.example.net | This regular expression has an unescaped '.' before 'example.net', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:44:17:44:109 | "^protos?://(localhost\|.+.exam..." | here |
| tst-IncompleteHostnameRegExp.rb:44:55:44:70 | .+.example-a.com | This regular expression has an unescaped '.' before 'example-a.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:44:17:44:109 | "^protos?://(localhost\|.+.exam..." | here |
| tst-IncompleteHostnameRegExp.rb:44:72:44:87 | .+.example-b.com | This regular expression has an unescaped '.' before 'example-b.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:44:17:44:109 | "^protos?://(localhost\|.+.exam..." | here |
+| tst-IncompleteHostnameRegExp.rb:48:42:48:67 | ^https?://.+.example\\.com/ | This string, which is used as a regular expression $@, has an unescaped '.' before 'example\\.com/', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:48:13:48:69 | ... + ... | here |
+| tst-IncompleteHostnameRegExp.rb:48:42:48:67 | ^https?://.+.example\\.com/ | This string, which is used as a regular expression $@, has an unrestricted wildcard '.+' which may cause 'example\\.com/' to be matched anywhere in the URL, outside the hostname. | tst-IncompleteHostnameRegExp.rb:48:13:48:69 | ... + ... | here |
| tst-IncompleteHostnameRegExp.rb:59:5:59:20 | foo.example\\.com | This regular expression has an unescaped '.' before 'example\\.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.rb:59:2:59:32 | /^(foo.example\\.com\|whatever)$/ | here |
From 1a51f0cf568d0da51604ad338b27677a09de617b Mon Sep 17 00:00:00 2001
From: Arthur Baars
Date: Wed, 16 Mar 2022 14:32:05 +0100
Subject: [PATCH 11/13] Ruby: regex: fix getGroupNumber
non-capture groups should not have a group number
---
ruby/ql/lib/codeql/ruby/security/performance/ParseRegExp.qll | 3 ++-
ruby/ql/test/library-tests/regexp/regexp.expected | 3 ---
.../ql/test/query-tests/security/cwe-116/BadTagFilter.expected | 2 +-
3 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/ruby/ql/lib/codeql/ruby/security/performance/ParseRegExp.qll b/ruby/ql/lib/codeql/ruby/security/performance/ParseRegExp.qll
index b40c1fc4d50..52265e1c445 100644
--- a/ruby/ql/lib/codeql/ruby/security/performance/ParseRegExp.qll
+++ b/ruby/ql/lib/codeql/ruby/security/performance/ParseRegExp.qll
@@ -480,6 +480,7 @@ abstract class RegExp extends AST::StringlikeLiteral {
/** Gets the number of the group in start,end */
int getGroupNumber(int start, int end) {
this.group(start, end) and
+ not this.nonCapturingGroupStart(start, _) and
result =
count(int i | this.group(i, _) and i < start and not this.nonCapturingGroupStart(i, _)) + 1
}
@@ -583,7 +584,7 @@ abstract class RegExp extends AST::StringlikeLiteral {
private predicate nonCapturingGroupStart(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
- this.getChar(start + 2) = ":" and
+ this.getChar(start + 2) = [":", "=", "<", "!", "#"] and
end = start + 3
}
diff --git a/ruby/ql/test/library-tests/regexp/regexp.expected b/ruby/ql/test/library-tests/regexp/regexp.expected
index 35c85b0132f..4d1837bedea 100644
--- a/ruby/ql/test/library-tests/regexp/regexp.expected
+++ b/ruby/ql/test/library-tests/regexp/regexp.expected
@@ -6,11 +6,8 @@ groupNumber
| regexp.rb:46:2:46:6 | (foo) | 1 |
| regexp.rb:47:4:47:8 | (o\|b) | 1 |
| regexp.rb:48:2:48:9 | (a\|b\|cd) | 1 |
-| regexp.rb:49:2:49:7 | (?::+) | 1 |
-| regexp.rb:52:2:52:11 | (?\\w+) | 1 |
| regexp.rb:53:2:53:12 | (?'foo'fo+) | 1 |
| regexp.rb:56:2:56:5 | (a+) | 1 |
-| regexp.rb:57:2:57:11 | (?q+) | 1 |
term
| regexp.rb:5:2:5:4 | abc | RegExpConstant,RegExpNormalChar |
| regexp.rb:8:2:8:2 | a | RegExpConstant,RegExpNormalChar |
diff --git a/ruby/ql/test/query-tests/security/cwe-116/BadTagFilter.expected b/ruby/ql/test/query-tests/security/cwe-116/BadTagFilter.expected
index fe1b106a083..edd6347e2d3 100644
--- a/ruby/ql/test/query-tests/security/cwe-116/BadTagFilter.expected
+++ b/ruby/ql/test/query-tests/security/cwe-116/BadTagFilter.expected
@@ -11,4 +11,4 @@
| test.rb:15:6:15:39 | . |
| test.rb:17:6:17:40 | . |
| test.rb:18:6:18:48 | <(?:!--([\\S\|\\s]*?)-->)\|([^\\/\\s>]+)[\\S\\s]*?> | Comments ending with --> are matched differently from comments ending with --!>. The first is matched with capture group 1 and comments ending with --!> are matched with capture group 2. |
-| test.rb:19:6:19:147 | <(?:(?:\\/([^>]+)>)\|(?:!--([\\S\|\\s]*?)-->)\|(?:([^\\/\\s>]+)((?:\\s+[\\w\\-:.]+(?:\\s*=\\s*?(?:(?:"[^"]*")\|(?:'[^']*')\|[^\\s"'\\/>]+))?)*)[\\S\\s]*?(\\/?)>)) | Comments ending with --> are matched differently from comments ending with --!>. The first is matched with capture group 2 and comments ending with --!> are matched with capture group 1, 3, 4, 5. |
+| test.rb:19:6:19:147 | <(?:(?:\\/([^>]+)>)\|(?:!--([\\S\|\\s]*?)-->)\|(?:([^\\/\\s>]+)((?:\\s+[\\w\\-:.]+(?:\\s*=\\s*?(?:(?:"[^"]*")\|(?:'[^']*')\|[^\\s"'\\/>]+))?)*)[\\S\\s]*?(\\/?)>)) | Comments ending with --> are matched differently from comments ending with --!>. The first is matched with capture group 2 and comments ending with --!> are matched with capture group 3, 4. |
From 6d245914168abc5d7e1cb610295a0d6890ca8749 Mon Sep 17 00:00:00 2001
From: Arthur Baars
Date: Fri, 18 Mar 2022 13:02:55 +0100
Subject: [PATCH 12/13] Revert "Python: switch to shared implementation of
IncompleteHostnameRegExp.ql"
This reverts commit ce50f35ddaa0598272624032cd72aed8ae539b7f.
---
config/identical-files.json | 3 +-
python/ql/lib/semmle/python/RegexTreeView.qll | 40 ----
.../Security/CWE-020/HostnameRegexpShared.qll | 202 ------------------
.../CWE-020/HostnameRegexpSpecific.qll | 2 -
.../CWE-020/IncompleteHostnameRegExp.ql | 32 ++-
.../IncompleteHostnameRegExp.expected | 2 +-
6 files changed, 31 insertions(+), 250 deletions(-)
delete mode 100644 python/ql/src/Security/CWE-020/HostnameRegexpShared.qll
delete mode 100644 python/ql/src/Security/CWE-020/HostnameRegexpSpecific.qll
diff --git a/config/identical-files.json b/config/identical-files.json
index ff4129d9776..75b86933ebb 100644
--- a/config/identical-files.json
+++ b/config/identical-files.json
@@ -518,7 +518,6 @@
],
"Hostname Regexp queries": [
"javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll",
- "ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll",
- "python/ql/src/Security/CWE-020/HostnameRegexpShared.qll"
+ "ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll"
]
}
\ No newline at end of file
diff --git a/python/ql/lib/semmle/python/RegexTreeView.qll b/python/ql/lib/semmle/python/RegexTreeView.qll
index 019089ecbc2..95d983f5e88 100644
--- a/python/ql/lib/semmle/python/RegexTreeView.qll
+++ b/python/ql/lib/semmle/python/RegexTreeView.qll
@@ -2,7 +2,6 @@
import python
private import semmle.python.regex
-private import semmle.python.dataflow.new.DataFlow
/**
* An element containing a regular expression term, that is, either
@@ -49,19 +48,6 @@ newtype TRegExpParent =
/** A back reference */
TRegExpBackRef(Regex re, int start, int end) { re.backreference(start, end) }
-/**
- * Provides utility predicates related to regular expressions.
- */
-module RegExpPatterns {
- /**
- * Gets a pattern that matches common top-level domain names in lower case.
- */
- string getACommonTld() {
- // according to ranking by http://google.com/search?q=site:.<>
- result = "(?:com|org|edu|gov|uk|net|io)(?![a-z0-9])"
- }
-}
-
/**
* An element containing a regular expression term, that is, either
* a string literal (parsed as a regular expression)
@@ -459,8 +445,6 @@ class RegExpAlt extends RegExpTerm, TRegExpAlt {
override string getPrimaryQLClass() { result = "RegExpAlt" }
}
-class RegExpCharEscape = RegExpEscape;
-
/**
* An escaped regular expression term, that is, a regular expression
* term starting with a backslash, which is not a backreference.
@@ -767,9 +751,6 @@ class RegExpGroup extends RegExpTerm, TRegExpGroup {
*/
int getNumber() { result = re.getGroupNumber(start, end) }
- /** Holds if this is a capture group. */
- predicate isCapture() { exists(this.getNumber()) }
-
/** Holds if this is a named capture group. */
predicate isNamed() { exists(this.getName()) }
@@ -1028,24 +1009,3 @@ class RegExpBackRef extends RegExpTerm, TRegExpBackRef {
/** Gets the parse tree resulting from parsing `re`, if such has been constructed. */
RegExpTerm getParsedRegExp(StrConst re) { result.getRegex() = re and result.isRootTerm() }
-
-/**
- * A node whose value may flow to a position where it is interpreted
- * as a part of a regular expression.
- */
-class RegExpPatternSource extends DataFlow::CfgNode {
- private Regex astNode;
-
- RegExpPatternSource() { astNode = this.asExpr() }
-
- /**
- * Gets a node where the pattern of this node is parsed as a part of
- * a regular expression.
- */
- DataFlow::Node getAParse() { result = this }
-
- /**
- * Gets the root term of the regular expression parsed from this pattern.
- */
- RegExpTerm getRegExpTerm() { result.getRegex() = astNode }
-}
diff --git a/python/ql/src/Security/CWE-020/HostnameRegexpShared.qll b/python/ql/src/Security/CWE-020/HostnameRegexpShared.qll
deleted file mode 100644
index 2192951f76d..00000000000
--- a/python/ql/src/Security/CWE-020/HostnameRegexpShared.qll
+++ /dev/null
@@ -1,202 +0,0 @@
-/**
- * Provides predicates for reasoning about regular expressions
- * that match URLs and hostname patterns.
- */
-
-private import HostnameRegexpSpecific
-
-/**
- * Holds if the given constant is unlikely to occur in the origin part of a URL.
- */
-predicate isConstantInvalidInsideOrigin(RegExpConstant term) {
- // Look for any of these cases:
- // - A character that can't occur in the origin
- // - Two dashes in a row
- // - A colon that is not part of port or scheme separator
- // - A slash that is not part of scheme separator
- term.getValue().regexpMatch(".*(?:[^a-zA-Z0-9.:/-]|--|:[^0-9/]|(?
Date: Fri, 18 Mar 2022 14:00:10 +0100
Subject: [PATCH 13/13] Ruby/JS add missing ^ in qhelp
---
.../ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp | 2 +-
.../src/queries/security/cwe-020/IncompleteHostnameRegExp.qhelp | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp b/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp
index 86637d140df..3b49628c84f 100644
--- a/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp
+++ b/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp
@@ -59,7 +59,7 @@
Address this vulnerability by escaping .
- appropriately: let regex = /((www|beta)\.)?example\.com/.
+ appropriately: let regex = /^((www|beta)\.)?example\.com/.
diff --git a/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.qhelp b/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.qhelp
index cb242aa8c61..44ec2175709 100644
--- a/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.qhelp
+++ b/ruby/ql/src/queries/security/cwe-020/IncompleteHostnameRegExp.qhelp
@@ -59,7 +59,7 @@
Address this vulnerability by escaping .
- appropriately: regex = /((www|beta)\.)?example\.com/.
+ appropriately: regex = /^((www|beta)\.)?example\.com/.