Skip to content

Commit f67d0bc

Browse files
committed
put the shared HostnameRegexp code in the shared regex pack
1 parent 30451ee commit f67d0bc

File tree

20 files changed

+369
-653
lines changed

20 files changed

+369
-653
lines changed

config/identical-files.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -531,11 +531,6 @@
531531
"ruby/ql/lib/codeql/ruby/internal/ConceptsShared.qll",
532532
"javascript/ql/lib/semmle/javascript/internal/ConceptsShared.qll"
533533
],
534-
"Hostname Regexp queries": [
535-
"javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll",
536-
"python/ql/src/Security/CWE-020/HostnameRegexpShared.qll",
537-
"ruby/ql/src/queries/security/cwe-020/HostnameRegexpShared.qll"
538-
],
539534
"ApiGraphModels": [
540535
"javascript/ql/lib/semmle/javascript/frameworks/data/internal/ApiGraphModels.qll",
541536
"ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModels.qll",

java/ql/lib/semmle/code/java/regex/RegexTreeView.qll

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,8 @@ module Impl implements RegexTreeViewSig {
558558
}
559559
}
560560

561+
class RegExpCharEscape = RegExpEscape;
562+
561563
/**
562564
* A word boundary, that is, a regular expression term of the form `\b`.
563565
*/
@@ -868,6 +870,9 @@ module Impl implements RegexTreeViewSig {
868870
predicate isNamedGroupOfLiteral(RegExpLiteral lit, string name) {
869871
lit = this.getLiteral() and name = this.getName()
870872
}
873+
874+
/** Holds if this is a capture group. */
875+
predicate isCapture() { exists(this.getNumber()) }
871876
}
872877

873878
/**
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Provides predicates for reasoning about regular expressions
3+
* that match URLs and hostname patterns.
4+
*/
5+
6+
private import javascript as JS
7+
private import semmle.javascript.security.regexp.RegExpTreeView::RegExpTreeView as TreeImpl
8+
private import semmle.javascript.Regexp as RegExp
9+
private import codeql.regex.HostnameRegexp as Shared
10+
11+
private module Impl implements Shared::HostnameRegexpSig<TreeImpl> {
12+
class DataFlowNode = JS::DataFlow::Node;
13+
14+
class RegExpPatternSource = RegExp::RegExpPatternSource;
15+
16+
string getACommonTld() { result = RegExp::RegExpPatterns::getACommonTld() }
17+
}
18+
19+
import Shared::Make<TreeImpl, Impl>

javascript/ql/src/Security/CWE-020/HostnameRegexpShared.qll

Lines changed: 2 additions & 197 deletions
Original file line numberDiff line numberDiff line change
@@ -3,200 +3,5 @@
33
* that match URLs and hostname patterns.
44
*/
55

6-
private import HostnameRegexpSpecific
7-
8-
/**
9-
* Holds if the given constant is unlikely to occur in the origin part of a URL.
10-
*/
11-
predicate isConstantInvalidInsideOrigin(RegExpConstant term) {
12-
// Look for any of these cases:
13-
// - A character that can't occur in the origin
14-
// - Two dashes in a row
15-
// - A colon that is not part of port or scheme separator
16-
// - A slash that is not part of scheme separator
17-
term.getValue().regexpMatch(".*(?:[^a-zA-Z0-9.:/-]|--|:[^0-9/]|(?<![/:]|^)/).*")
18-
}
19-
20-
/** Holds if `term` is a dot constant of form `\.` or `[.]`. */
21-
predicate isDotConstant(RegExpTerm term) {
22-
term.(RegExpCharEscape).getValue() = "."
23-
or
24-
exists(RegExpCharacterClass cls |
25-
term = cls and
26-
not cls.isInverted() and
27-
cls.getNumChild() = 1 and
28-
cls.getAChild().(RegExpConstant).getValue() = "."
29-
)
30-
}
31-
32-
/** Holds if `term` is a wildcard `.` or an actual `.` character. */
33-
predicate isDotLike(RegExpTerm term) {
34-
term instanceof RegExpDot
35-
or
36-
isDotConstant(term)
37-
}
38-
39-
/** Holds if `term` will only ever be matched against the beginning of the input. */
40-
predicate matchesBeginningOfString(RegExpTerm term) {
41-
term.isRootTerm()
42-
or
43-
exists(RegExpTerm parent | matchesBeginningOfString(parent) |
44-
term = parent.(RegExpSequence).getChild(0)
45-
or
46-
parent.(RegExpSequence).getChild(0) instanceof RegExpCaret and
47-
term = parent.(RegExpSequence).getChild(1)
48-
or
49-
term = parent.(RegExpAlt).getAChild()
50-
or
51-
term = parent.(RegExpGroup).getAChild()
52-
)
53-
}
54-
55-
/**
56-
* Holds if the given sequence `seq` contains top-level domain preceded by a dot, such as `.com`,
57-
* excluding cases where this is at the very beginning of the regexp.
58-
*
59-
* `i` is bound to the index of the last child in the top-level domain part.
60-
*/
61-
predicate hasTopLevelDomainEnding(RegExpSequence seq, int i) {
62-
seq.getChild(i)
63-
.(RegExpConstant)
64-
.getValue()
65-
.regexpMatch("(?i)" + RegExpPatterns::getACommonTld() + "(:\\d+)?([/?#].*)?") and
66-
isDotLike(seq.getChild(i - 1)) and
67-
not (i = 1 and matchesBeginningOfString(seq))
68-
}
69-
70-
/**
71-
* Holds if the given regular expression term contains top-level domain preceded by a dot,
72-
* such as `.com`.
73-
*/
74-
predicate hasTopLevelDomainEnding(RegExpSequence seq) { hasTopLevelDomainEnding(seq, _) }
75-
76-
/**
77-
* Holds if `term` will always match a hostname, that is, all disjunctions contain
78-
* a hostname pattern that isn't inside a quantifier.
79-
*/
80-
predicate alwaysMatchesHostname(RegExpTerm term) {
81-
hasTopLevelDomainEnding(term, _)
82-
or
83-
// `localhost` is considered a hostname pattern, but has no TLD
84-
term.(RegExpConstant).getValue().regexpMatch("\\blocalhost\\b")
85-
or
86-
not term instanceof RegExpAlt and
87-
not term instanceof RegExpQuantifier and
88-
alwaysMatchesHostname(term.getAChild())
89-
or
90-
alwaysMatchesHostnameAlt(term)
91-
}
92-
93-
/** Holds if every child of `alt` contains a hostname pattern. */
94-
predicate alwaysMatchesHostnameAlt(RegExpAlt alt) {
95-
alwaysMatchesHostnameAlt(alt, alt.getNumChild() - 1)
96-
}
97-
98-
/**
99-
* Holds if the first `i` children of `alt` contains a hostname pattern.
100-
*
101-
* This is used instead of `forall` to avoid materializing the set of alternatives
102-
* that don't contains hostnames, which is much larger.
103-
*/
104-
predicate alwaysMatchesHostnameAlt(RegExpAlt alt, int i) {
105-
alwaysMatchesHostname(alt.getChild(0)) and i = 0
106-
or
107-
alwaysMatchesHostnameAlt(alt, i - 1) and
108-
alwaysMatchesHostname(alt.getChild(i))
109-
}
110-
111-
/**
112-
* Holds if `term` occurs inside a quantifier or alternative (and thus
113-
* can not be expected to correspond to a unique match), or as part of
114-
* a lookaround assertion (which are rarely used for capture groups).
115-
*/
116-
predicate isInsideChoiceOrSubPattern(RegExpTerm term) {
117-
exists(RegExpParent parent | parent = term.getParent() |
118-
parent instanceof RegExpAlt
119-
or
120-
parent instanceof RegExpQuantifier
121-
or
122-
parent instanceof RegExpSubPattern
123-
or
124-
isInsideChoiceOrSubPattern(parent)
125-
)
126-
}
127-
128-
/**
129-
* Holds if `group` is likely to be used as a capture group.
130-
*/
131-
predicate isLikelyCaptureGroup(RegExpGroup group) {
132-
group.isCapture() and
133-
not isInsideChoiceOrSubPattern(group)
134-
}
135-
136-
/**
137-
* Holds if `seq` contains two consecutive dots `..` or escaped dots.
138-
*
139-
* At least one of these dots is not intended to be a subdomain separator,
140-
* so we avoid flagging the pattern in this case.
141-
*/
142-
predicate hasConsecutiveDots(RegExpSequence seq) {
143-
exists(int i |
144-
isDotLike(seq.getChild(i)) and
145-
isDotLike(seq.getChild(i + 1))
146-
)
147-
}
148-
149-
predicate isIncompleteHostNameRegExpPattern(RegExpTerm regexp, RegExpSequence seq, string msg) {
150-
seq = regexp.getAChild*() and
151-
exists(RegExpDot unescapedDot, int i, string hostname |
152-
hasTopLevelDomainEnding(seq, i) and
153-
not isConstantInvalidInsideOrigin(seq.getChild([0 .. i - 1]).getAChild*()) and
154-
not isLikelyCaptureGroup(seq.getChild([i .. seq.getNumChild() - 1]).getAChild*()) and
155-
unescapedDot = seq.getChild([0 .. i - 1]).getAChild*() and
156-
unescapedDot != seq.getChild(i - 1) and // Should not be the '.' immediately before the TLD
157-
not hasConsecutiveDots(unescapedDot.getParent()) and
158-
hostname =
159-
seq.getChild(i - 2).getRawValue() + seq.getChild(i - 1).getRawValue() +
160-
seq.getChild(i).getRawValue()
161-
|
162-
if unescapedDot.getParent() instanceof RegExpQuantifier
163-
then
164-
// `.*\.example.com` can match `evil.com/?x=.example.com`
165-
//
166-
// This problem only occurs when the pattern is applied against a full URL, not just a hostname/origin.
167-
// We therefore check if the pattern includes a suffix after the TLD, such as `.*\.example.com/`.
168-
// Note that a post-anchored pattern (`.*\.example.com$`) will usually fail to match a full URL,
169-
// and patterns with neither a suffix nor an anchor fall under the purview of MissingRegExpAnchor.
170-
seq.getChild(0) instanceof RegExpCaret and
171-
not seq.getAChild() instanceof RegExpDollar and
172-
seq.getChild([i .. i + 1]).(RegExpConstant).getValue().regexpMatch(".*[/?#].*") and
173-
msg =
174-
"has an unrestricted wildcard '" + unescapedDot.getParent().(RegExpQuantifier).getRawValue()
175-
+ "' which may cause '" + hostname +
176-
"' to be matched anywhere in the URL, outside the hostname."
177-
else
178-
msg =
179-
"has an unescaped '.' before '" + hostname +
180-
"', so it might match more hosts than expected."
181-
)
182-
}
183-
184-
predicate incompleteHostnameRegExp(
185-
RegExpSequence hostSequence, string message, DataFlow::Node aux, string label
186-
) {
187-
exists(RegExpPatternSource re, RegExpTerm regexp, string msg, string kind |
188-
regexp = re.getRegExpTerm() and
189-
isIncompleteHostNameRegExpPattern(regexp, hostSequence, msg) and
190-
(
191-
if re.getAParse() != re
192-
then (
193-
kind = "string, which is used as a regular expression $@," and
194-
aux = re.getAParse()
195-
) else (
196-
kind = "regular expression" and aux = re
197-
)
198-
)
199-
|
200-
message = "This " + kind + " " + msg and label = "here"
201-
)
202-
}
6+
deprecated import semmle.javascript.security.regexp.HostnameRegexp as Dep
7+
import Dep

javascript/ql/src/Security/CWE-020/HostnameRegexpSpecific.qll

Lines changed: 0 additions & 1 deletion
This file was deleted.

javascript/ql/src/Security/CWE-020/IncompleteHostnameRegExp.ql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@
1111
* external/cwe/cwe-020
1212
*/
1313

14-
import HostnameRegexpShared
14+
private import semmle.javascript.security.regexp.HostnameRegexp as HostnameRegexp
1515

16-
query predicate problems = incompleteHostnameRegExp/4;
16+
query predicate problems = HostnameRegexp::incompleteHostnameRegExp/4;

javascript/ql/src/Security/CWE-020/MissingRegExpAnchor.ql

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,10 @@
1111
* external/cwe/cwe-020
1212
*/
1313

14-
import javascript
15-
import HostnameRegexpShared
16-
17-
/** Holds if `term` is one of the transitive left children of a regexp. */
18-
predicate isLeftArmTerm(RegExpTerm term) {
19-
term.isRootTerm()
20-
or
21-
exists(RegExpTerm parent |
22-
term = parent.getChild(0) and
23-
isLeftArmTerm(parent)
24-
)
25-
}
26-
27-
/** Holds if `term` is one of the transitive right children of a regexp. */
28-
predicate isRightArmTerm(RegExpTerm term) {
29-
term.isRootTerm()
30-
or
31-
exists(RegExpTerm parent |
32-
term = parent.getLastChild() and
33-
isRightArmTerm(parent)
34-
)
35-
}
14+
private import javascript
15+
private import semmle.javascript.security.regexp.HostnameRegexp
3616

17+
// TODO: Share the below code.
3718
/**
3819
* Holds if `term` is an anchor that is not the first or last node
3920
* in its tree.

python/ql/lib/semmle/python/RegexTreeView.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ module Impl implements RegexTreeViewSig {
454454
override string getPrimaryQLClass() { result = "RegExpAlt" }
455455
}
456456

457-
additional class RegExpCharEscape = RegExpEscape;
457+
class RegExpCharEscape = RegExpEscape;
458458

459459
/**
460460
* An escaped regular expression term, that is, a regular expression
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Provides predicates for reasoning about regular expressions
3+
* that match URLs and hostname patterns.
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.DataFlow
8+
private import semmle.python.RegexTreeView::RegexTreeView as TreeImpl
9+
private import semmle.python.dataflow.new.Regexp as Regexp
10+
private import codeql.regex.HostnameRegexp as Shared
11+
12+
private module Impl implements Shared::HostnameRegexpSig<TreeImpl> {
13+
class DataFlowNode = DataFlow::Node;
14+
15+
class RegExpPatternSource = Regexp::RegExpPatternSource;
16+
17+
string getACommonTld() { result = Regexp::RegExpPatterns::getACommonTld() }
18+
}
19+
20+
import Shared::Make<TreeImpl, Impl>

0 commit comments

Comments
 (0)