diff --git a/change-notes/1.21/analysis-javascript.md b/change-notes/1.21/analysis-javascript.md index 276ef11d4e60..e1b4eaea6b0a 100644 --- a/change-notes/1.21/analysis-javascript.md +++ b/change-notes/1.21/analysis-javascript.md @@ -26,6 +26,7 @@ | Client-side URL redirect | More results and fewer false-positive results | This rule now recognizes additional uses of the document URL. This rule now treats URLs as safe in more cases where the hostname cannot be tampered with. | | Double escaping or unescaping | More results | This rule now considers the flow of regular expressions literals. | | Expression has no effect | Fewer false-positive results | This rule now treats uses of `Object.defineProperty` more conservatively. | +| Incomplete regular expression for hostnames | More results | This rule now tracks regular expressions for host names further. | | Incomplete string escaping or encoding | More results | This rule now considers the flow of regular expressions literals. | | Replacement of a substring with itself | More results | This rule now considers the flow of regular expressions literals. | | Server-side URL redirect | Fewer false-positive results | This rule now treats URLs as safe in more cases where the hostname cannot be tampered with. | diff --git a/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp b/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp index 71fefb488fa3..771c446c66f5 100644 --- a/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp +++ b/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.qhelp @@ -59,13 +59,14 @@

Address this vulnerability by escaping . - appropriately: let regex = /(www|beta|)\.example\.com/. + appropriately: let regex = /((www|beta)\.)?example\.com/.

+
  • MDN: Regular Expressions
  • OWASP: SSRF
  • OWASP: XSS Unvalidated Redirects and Forwards Cheat Sheet.
  • diff --git a/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.ql b/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.ql index 049b7f4c64a7..70589092064e 100644 --- a/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.ql +++ b/javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.ql @@ -13,16 +13,35 @@ import javascript /** - * A taint tracking configuration for incomplete hostname regular expressions sources. + * Gets a node whose value may flow (inter-procedurally) to a position where it is interpreted + * as a regular expression. */ -class Configuration extends TaintTracking::Configuration { - Configuration() { this = "IncompleteHostnameRegExpTracking" } +DataFlow::Node regExpSource(DataFlow::Node re, DataFlow::TypeBackTracker t) { + t.start() and + re = result and + isInterpretedAsRegExp(result) + or + exists(DataFlow::TypeBackTracker t2, DataFlow::Node succ | succ = regExpSource(re, t2) | + t2 = t.smallstep(result, succ) + or + any(TaintTracking::AdditionalTaintStep dts).step(result, succ) and + t = t2 + ) +} - override predicate isSource(DataFlow::Node source) { - isIncompleteHostNameRegExpPattern(source.getStringValue(), _) - } +DataFlow::Node regExpSource(DataFlow::Node re) { + result = regExpSource(re, DataFlow::TypeBackTracker::end()) +} - override predicate isSink(DataFlow::Node sink) { isInterpretedAsRegExp(sink) } +/** Holds if `re` is a regular expression with value `pattern`. */ +predicate regexp(DataFlow::Node re, string pattern, string kind, DataFlow::Node aux) { + re.asExpr().(RegExpLiteral).getValue() = pattern and + kind = "regular expression" and + aux = re + or + re = regExpSource(aux) and + pattern = re.getStringValue() and + kind = "string, which is used as a regular expression $@," } /** @@ -36,22 +55,14 @@ predicate isIncompleteHostNameRegExpPattern(string pattern, string hostPart) { // an unescaped single `.` "(?> - result = "com|org|edu|gov|uk|net|io" + result = "(?:com|org|edu|gov|uk|net|io)(?![a-z0-9])" } } diff --git a/javascript/ql/src/semmle/javascript/dataflow/Sources.qll b/javascript/ql/src/semmle/javascript/dataflow/Sources.qll index b3c9e7353d5f..43c512885aad 100644 --- a/javascript/ql/src/semmle/javascript/dataflow/Sources.qll +++ b/javascript/ql/src/semmle/javascript/dataflow/Sources.qll @@ -179,10 +179,7 @@ class SourceNode extends DataFlow::Node { */ pragma[inline] DataFlow::SourceNode backtrack(TypeBackTracker t2, TypeBackTracker t) { - exists(StepSummary summary | - StepSummary::step(result, this, summary) and - t = t2.prepend(summary) - ) + t2 = t.step(result, this) } } diff --git a/javascript/ql/src/semmle/javascript/dataflow/TypeTracking.qll b/javascript/ql/src/semmle/javascript/dataflow/TypeTracking.qll index b7bd316d4a42..b5637212868a 100644 --- a/javascript/ql/src/semmle/javascript/dataflow/TypeTracking.qll +++ b/javascript/ql/src/semmle/javascript/dataflow/TypeTracking.qll @@ -41,13 +41,9 @@ class StepSummary extends TStepSummary { or this instanceof ReturnStep and result = "return" or - exists(string prop | this = StoreStep(prop) | - result = "store " + prop - ) + exists(string prop | this = StoreStep(prop) | result = "store " + prop) or - exists(string prop | this = LoadStep(prop) | - result = "load" + prop - ) + exists(string prop | this = LoadStep(prop) | result = "load " + prop) } } @@ -56,40 +52,44 @@ module StepSummary { * INTERNAL: Use `SourceNode.track()` or `SourceNode.backtrack()` instead. */ predicate step(DataFlow::SourceNode pred, DataFlow::SourceNode succ, StepSummary summary) { - exists(DataFlow::Node predNode | pred.flowsTo(predNode) | - // Flow through properties of objects - propertyFlowStep(predNode, succ) and - summary = LevelStep() - or - // Flow through global variables - globalFlowStep(predNode, succ) and - summary = LevelStep() - or - // Flow into function - callStep(predNode, succ) and - summary = CallStep() - or - // Flow out of function - returnStep(predNode, succ) and - summary = ReturnStep() - or - // Flow through an instance field between members of the same class - DataFlow::localFieldStep(predNode, succ) and - summary = LevelStep() + exists(DataFlow::Node mid | pred.flowsTo(mid) | smallstep(mid, succ, summary)) + } + + /** + * INTERNAL: Use `TypeBackTracker.smallstep()` instead. + */ + predicate smallstep(DataFlow::Node pred, DataFlow::Node succ, StepSummary summary) { + // Flow through properties of objects + propertyFlowStep(pred, succ) and + summary = LevelStep() + or + // Flow through global variables + globalFlowStep(pred, succ) and + summary = LevelStep() + or + // Flow into function + callStep(pred, succ) and + summary = CallStep() + or + // Flow out of function + returnStep(pred, succ) and + summary = ReturnStep() + or + // Flow through an instance field between members of the same class + DataFlow::localFieldStep(pred, succ) and + summary = LevelStep() + or + exists(string prop | + basicStoreStep(pred, succ, prop) and + summary = StoreStep(prop) or - exists(string prop | - basicStoreStep(predNode, succ, prop) and - summary = StoreStep(prop) - or - loadStep(predNode, succ, prop) and - summary = LoadStep(prop) - ) + loadStep(pred, succ, prop) and + summary = LoadStep(prop) ) } } -private newtype TTypeTracker = - MkTypeTracker(Boolean hasCall, OptionalPropertyName prop) +private newtype TTypeTracker = MkTypeTracker(Boolean hasCall, OptionalPropertyName prop) /** * EXPERIMENTAL. @@ -136,9 +136,7 @@ class TypeTracker extends TTypeTracker { or step = LoadStep(prop) and result = MkTypeTracker(hasCall, "") or - exists(string p | - step = StoreStep(p) and prop = "" and result = MkTypeTracker(hasCall, p) - ) + exists(string p | step = StoreStep(p) and prop = "" and result = MkTypeTracker(hasCall, p)) } /** Gets a textual representation of this summary. */ @@ -180,8 +178,7 @@ module TypeTracker { TypeTracker end() { result.end() } } -private newtype TTypeBackTracker = - MkTypeBackTracker(Boolean hasReturn, OptionalPropertyName prop) +private newtype TTypeBackTracker = MkTypeBackTracker(Boolean hasReturn, OptionalPropertyName prop) /** * EXPERIMENTAL. @@ -192,7 +189,7 @@ private newtype TTypeBackTracker = * therefore expected to called with a certain type of value. * * Note that type back-tracking does not provide a source/sink relation, that is, - * it may determine that a node will be used in an API call somwwhere, but it won't + * it may determine that a node will be used in an API call somewhere, but it won't * determine exactly where that use was, or the path that led to the use. * * It is recommended that all uses of this type is written on the following form, @@ -203,7 +200,7 @@ private newtype TTypeBackTracker = * result = (< some API call >).getArgument(< n >).getALocalSource() * or * exists (DataFlow::TypeBackTracker t2 | - * result = myCallback(t2).backtrack(t2, t) + * t2 = t.step(result, myCallback(t2)) * ) * } * @@ -225,9 +222,7 @@ class TypeBackTracker extends TTypeBackTracker { or step = ReturnStep() and result = MkTypeBackTracker(true, prop) or - exists(string p | - step = LoadStep(p) and prop = "" and result = MkTypeBackTracker(hasReturn, p) - ) + exists(string p | step = LoadStep(p) and prop = "" and result = MkTypeBackTracker(hasReturn, p)) or step = StoreStep(prop) and result = MkTypeBackTracker(hasReturn, "") } @@ -265,6 +260,37 @@ class TypeBackTracker extends TTypeBackTracker { * This predicate is only defined if the type has not been tracked into a property. */ TypeBackTracker continue() { prop = "" and result = this } + + /** + * Gets the summary that corresponds to having taken a backwards + * heap and/or inter-procedural step from `succ` to `pred`. + */ + pragma[inline] + TypeBackTracker step(DataFlow::SourceNode pred, DataFlow::SourceNode succ) { + exists(StepSummary summary | + StepSummary::step(pred, succ, summary) and + this = result.prepend(summary) + ) + } + + /** + * Gets the summary that corresponds to having taken a backwards + * local, heap and/or inter-procedural step from `succ` to `pred`. + * + * Unlike `TypeBackTracker::step`, this predicate exposes all edges + * in the flowgraph, and not just the edges between + * `SourceNode`s. It may therefore be less performant. + */ + pragma[inline] + TypeBackTracker smallstep(DataFlow::Node pred, DataFlow::Node succ) { + exists(StepSummary summary | + StepSummary::smallstep(pred, succ, summary) and + this = result.prepend(summary) + ) + or + pred = succ.getAPredecessor() and + this = result + } } module TypeBackTracker { 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 cbb655c77bab..b34db52ed794 100644 --- a/javascript/ql/test/query-tests/Security/CWE-020/IncompleteHostnameRegExp.expected +++ b/javascript/ql/test/query-tests/Security/CWE-020/IncompleteHostnameRegExp.expected @@ -1,21 +1,24 @@ -| tst-IncompleteHostnameRegExp.js:3:2:3:28 | /http:\\ ... le.com/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:5:2:5:28 | /http:\\ ... le.net/ | This regular expression has an unescaped '.' before 'example.net', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:6:2:6:42 | /http:\\ ... 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:11:13:11:37 | "http:/ ... le.com" | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:12:10:12:34 | "http:/ ... le.com" | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:15:22:15:46 | "http:/ ... le.com" | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:17:13:17:31 | `test.example.com$` | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js: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.js:19:17:19:34 | 'test.example.com' | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:22:27:22:44 | 'test.example.com' | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:28:22:28:39 | 'test.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 ... =$\|\\/)/ | 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\\// | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:39:2:39:34 | /\\(http ... m\\/\\)/g | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:40:2:40:29 | /https? ... le.com/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:41:13:41:68 | '^http: ... e\\.com' | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:41:41:41:68 | '^https ... e\\.com' | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:42:13:42:61 | 'http[s ... \\/(.+)' | 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$/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:44:9:44:100 | 'protos ... ernal)' | This regular expression has an unescaped '.' before 'example-b.com', so it might match more hosts than expected. | -| tst-IncompleteHostnameRegExp.js:46:2:46:26 | /exampl ... le.com/ | This regular expression has an unescaped '.' before 'dev\|example.com', so it might match more hosts than expected. | +| tst-IncompleteHostnameRegExp.js:3:2:3:28 | /http:\\ ... le.com/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:3:2:3:28 | /http:\\ ... le.com/ | here | +| tst-IncompleteHostnameRegExp.js:5:2:5:28 | /http:\\ ... le.net/ | This regular expression has an unescaped '.' before 'example.net', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:5:2:5:28 | /http:\\ ... le.net/ | here | +| tst-IncompleteHostnameRegExp.js:6:2:6:42 | /http:\\ ... 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:42 | /http:\\ ... b).com/ | here | +| tst-IncompleteHostnameRegExp.js:11:13:11:37 | "http:/ ... le.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:11:13:11:37 | "http:/ ... le.com" | here | +| tst-IncompleteHostnameRegExp.js:12:10:12:35 | "^http: ... le.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:12:10:12:35 | "^http: ... le.com" | here | +| tst-IncompleteHostnameRegExp.js:17:13:17:31 | `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:17:13:17:31 | `test.example.com$` | here | +| tst-IncompleteHostnameRegExp.js:17:14:17: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.js:17:13:17:31 | `test.example.com$` | here | +| tst-IncompleteHostnameRegExp.js:19:17: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:27: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:23: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:30: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:2:37:54 | /^(http ... =$\|\\/)/ | 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:2:38:44 | /^(http ... 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:2:39:34 | /\\(http ... m\\/\\)/g | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:39:2:39:34 | /\\(http ... m\\/\\)/g | here | +| tst-IncompleteHostnameRegExp.js:40:2:40:29 | /https? ... le.com/ | This regular expression has an unescaped '.' before 'example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:40:2:40:29 | /https? ... le.com/ | here | +| tst-IncompleteHostnameRegExp.js:41:13:41:68 | '^http: ... e\\.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:41:13:41:68 | '^http: ... e\\.com' | here | +| tst-IncompleteHostnameRegExp.js:41:41:41:68 | '^https ... e\\.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:41:13:41:68 | '^http: ... e\\.com' | here | +| tst-IncompleteHostnameRegExp.js:42:13:42:61 | 'http[s ... \\/(.+)' | 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:42:13:42:61 | 'http[s ... \\/(.+)' | here | +| tst-IncompleteHostnameRegExp.js:43:2:43:33 | /^https ... e.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:9:44:100 | 'protos ... ernal)' | This string, which is used as a regular expression $@, has an unescaped '.' before 'example-b.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:44:9:44:100 | 'protos ... ernal)' | here | +| tst-IncompleteHostnameRegExp.js:46:2:46:26 | /exampl ... le.com/ | This regular expression has an unescaped '.' before 'dev\|example.com', so it might match more hosts than expected. | tst-IncompleteHostnameRegExp.js:46:2:46:26 | /exampl ... le.com/ | here | +| tst-IncompleteHostnameRegExp.js:48:13:48:68 | '^http: ... e\\.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:48:13:48:68 | '^http: ... e\\.com' | here | +| tst-IncompleteHostnameRegExp.js:48:41:48:68 | '^https ... e\\.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:48:13:48:68 | '^http: ... e\\.com' | here | +| tst-IncompleteHostnameRegExp.js:53:13:53:35 | 'test.' ... le.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:53:13:53:35 | 'test.' ... le.com' | 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 45e6476a22e1..708fbee16f29 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 @@ -9,10 +9,10 @@ /http:\/\/(?:.+)\\.test\\.example.com/; // NOT OK, but not yet supported with enough precision /http:\/\/test.example.com\/(?:.*)/; // OK new RegExp("http://test.example.com"); // NOT OK - s.match("http://test.example.com"); // NOT OK + 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(id(id(id("http://test.example.com")))); // NOT OK, but not supported by type tracking new RegExp(`test.example.com$`); // NOT OK @@ -22,16 +22,16 @@ let domain = { hostname: 'test.example.com' }; new RegExp(domain.hostname); - function convert(domain) { + function convert1(domain) { return new RegExp(domain.hostname); } - convert({ hostname: 'test.example.com' }); // NOT OK + convert1({ hostname: 'test.example.com' }); // NOT OK - let domains = [ { hostname: 'test.example.com' } ]; // NOT OK, but not yet supported - function convert(domain) { + let domains = [ { hostname: 'test.example.com' } ]; // NOT OK + function convert2(domain) { return new RegExp(domain.hostname); } - domains.map(d => convert(d)); + domains.map(d => convert2(d)); /(.+\.(?:example-a|example-b)\.com)/; // NOT OK, but not yet supported with enough precision /^(https?:)?\/\/((service|www).)?example.com(?=$|\/)/; // NOT OK @@ -44,4 +44,11 @@ RegExp('protos?://(localhost|.+.example.net|.+.example-a.com|.+.example-b.com|.+.example.internal)'); // NOT OK /example.dev|example.com/; // OK, but still flagged + + 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 });