Skip to content

Commit 15c54f6

Browse files
authored
Merge pull request #8354 from aibaars/incomplete-url-string-sanitization
Incomplete url string sanitization
2 parents 7e866ed + 85c4daa commit 15c54f6

19 files changed

+705
-59
lines changed

config/identical-files.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,10 @@
515515
"javascript/ql/lib/semmle/javascript/frameworks/data/internal/AccessPathSyntax.qll",
516516
"ruby/ql/lib/codeql/ruby/dataflow/internal/AccessPathSyntax.qll"
517517
],
518+
"IncompleteUrlSubstringSanitization": [
519+
"javascript/ql/src/Security/CWE-020/IncompleteUrlSubstringSanitization.qll",
520+
"ruby/ql/src/queries/security/cwe-020/IncompleteUrlSubstringSanitization.qll"
521+
],
518522
"Concepts Python/Ruby/JS": [
519523
"python/ql/lib/semmle/python/internal/ConceptsShared.qll",
520524
"ruby/ql/lib/codeql/ruby/internal/ConceptsShared.qll",

javascript/ql/lib/semmle/javascript/StringOps.qll

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,12 @@ module StringOps {
235235
*/
236236
class EndsWith extends DataFlow::Node instanceof EndsWith::Range {
237237
/**
238-
* Gets the `A` in `A.startsWith(B)`.
238+
* Gets the `A` in `A.endsWith(B)`.
239239
*/
240240
DataFlow::Node getBaseString() { result = super.getBaseString() }
241241

242242
/**
243-
* Gets the `B` in `A.startsWith(B)`.
243+
* Gets the `B` in `A.endsWith(B)`.
244244
*/
245245
DataFlow::Node getSubstring() { result = super.getSubstring() }
246246

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

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,60 +11,4 @@
1111
* external/cwe/cwe-020
1212
*/
1313

14-
import javascript
15-
private import semmle.javascript.dataflow.InferredTypes
16-
17-
/**
18-
* A check on a string for whether it contains a given substring, possibly with restrictions on the location of the substring.
19-
*/
20-
class SomeSubstringCheck extends DataFlow::Node {
21-
DataFlow::Node substring;
22-
23-
SomeSubstringCheck() {
24-
this.(StringOps::StartsWith).getSubstring() = substring or
25-
this.(StringOps::Includes).getSubstring() = substring or
26-
this.(StringOps::EndsWith).getSubstring() = substring
27-
}
28-
29-
/**
30-
* Gets the substring.
31-
*/
32-
DataFlow::Node getSubstring() { result = substring }
33-
}
34-
35-
from SomeSubstringCheck check, DataFlow::Node substring, string target, string msg
36-
where
37-
substring = check.getSubstring() and
38-
substring.mayHaveStringValue(target) and
39-
(
40-
// target contains a domain on a common TLD, and perhaps some other URL components
41-
target
42-
.regexpMatch("(?i)([a-z]*:?//)?\\.?([a-z0-9-]+\\.)+" + RegExpPatterns::getACommonTld() +
43-
"(:[0-9]+)?/?")
44-
or
45-
// target is a HTTP URL to a domain on any TLD
46-
target.regexpMatch("(?i)https?://([a-z0-9-]+\\.)+([a-z]+)(:[0-9]+)?/?")
47-
or
48-
// target is a HTTP URL to a domain on any TLD with path elements, and the check is an includes check
49-
check instanceof StringOps::Includes and
50-
target.regexpMatch("(?i)https?://([a-z0-9-]+\\.)+([a-z]+)(:[0-9]+)?/[a-z0-9/_-]+")
51-
) and
52-
(
53-
if check instanceof StringOps::StartsWith
54-
then msg = "may be followed by an arbitrary host name"
55-
else
56-
if check instanceof StringOps::EndsWith
57-
then msg = "may be preceded by an arbitrary host name"
58-
else msg = "can be anywhere in the URL, and arbitrary hosts may come before or after it"
59-
) and
60-
// whitelist
61-
not (
62-
// the leading dot in a subdomain sequence makes the suffix-check safe (if it is performed on the host of the url)
63-
check instanceof StringOps::EndsWith and
64-
target.regexpMatch("(?i)\\.([a-z0-9-]+)(\\.[a-z0-9-]+)+")
65-
or
66-
// the trailing port or slash makes the prefix-check safe
67-
check instanceof StringOps::StartsWith and
68-
target.regexpMatch(".*(:[0-9]+|/)")
69-
)
70-
select check, "'$@' " + msg + ".", substring, target
14+
import IncompleteUrlSubstringSanitization
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Incomplete URL substring sanitization
3+
*/
4+
5+
private import IncompleteUrlSubstringSanitizationSpecific
6+
7+
/**
8+
* A check on a string for whether it contains a given substring, possibly with restrictions on the location of the substring.
9+
*/
10+
class SomeSubstringCheck extends DataFlow::Node {
11+
DataFlow::Node substring;
12+
13+
SomeSubstringCheck() {
14+
this.(StringOps::StartsWith).getSubstring() = substring or
15+
this.(StringOps::Includes).getSubstring() = substring or
16+
this.(StringOps::EndsWith).getSubstring() = substring
17+
}
18+
19+
/**
20+
* Gets the substring.
21+
*/
22+
DataFlow::Node getSubstring() { result = substring }
23+
}
24+
25+
/** Holds if there is an incomplete URL substring sanitization problem */
26+
query predicate problems(
27+
SomeSubstringCheck check, string msg, DataFlow::Node substring, string target
28+
) {
29+
substring = check.getSubstring() and
30+
mayHaveStringValue(substring, target) and
31+
(
32+
// target contains a domain on a common TLD, and perhaps some other URL components
33+
target
34+
.regexpMatch("(?i)([a-z]*:?//)?\\.?([a-z0-9-]+\\.)+" + RegExpPatterns::getACommonTld() +
35+
"(:[0-9]+)?/?")
36+
or
37+
// target is a HTTP URL to a domain on any TLD
38+
target.regexpMatch("(?i)https?://([a-z0-9-]+\\.)+([a-z]+)(:[0-9]+)?/?")
39+
or
40+
// target is a HTTP URL to a domain on any TLD with path elements, and the check is an includes check
41+
check instanceof StringOps::Includes and
42+
target.regexpMatch("(?i)https?://([a-z0-9-]+\\.)+([a-z]+)(:[0-9]+)?/[a-z0-9/_-]+")
43+
) and
44+
(
45+
if check instanceof StringOps::StartsWith
46+
then msg = "'$@' may be followed by an arbitrary host name."
47+
else
48+
if check instanceof StringOps::EndsWith
49+
then msg = "'$@' may be preceded by an arbitrary host name."
50+
else msg = "'$@' can be anywhere in the URL, and arbitrary hosts may come before or after it."
51+
) and
52+
// whitelist
53+
not (
54+
// the leading dot in a subdomain sequence makes the suffix-check safe (if it is performed on the host of the url)
55+
check instanceof StringOps::EndsWith and
56+
target.regexpMatch("(?i)\\.([a-z0-9-]+)(\\.[a-z0-9-]+)+")
57+
or
58+
// the trailing port or slash makes the prefix-check safe
59+
check instanceof StringOps::StartsWith and
60+
target.regexpMatch(".*(:[0-9]+|/)")
61+
)
62+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import javascript
2+
import semmle.javascript.dataflow.InferredTypes
3+
4+
/** Holds if `node` may evaluate to `value` */
5+
predicate mayHaveStringValue(DataFlow::Node node, string value) { node.mayHaveStringValue(value) }
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Contains classes for recognizing array and string inclusion tests.
3+
*/
4+
5+
private import ruby
6+
private import codeql.ruby.DataFlow
7+
private import codeql.ruby.controlflow.CfgNodes
8+
9+
/**
10+
* An expression that checks if an element is contained in an array
11+
* or is a substring of another string.
12+
*
13+
* Examples:
14+
* ```
15+
* A.include?(B)
16+
* A.index(B) != nil
17+
* A.index(B) >= 0
18+
* ```
19+
*/
20+
class InclusionTest extends DataFlow::Node instanceof InclusionTest::Range {
21+
/** Gets the `A` in `A.include?(B)`. */
22+
final DataFlow::Node getContainerNode() { result = super.getContainerNode() }
23+
24+
/** Gets the `B` in `A.include?(B)`. */
25+
final DataFlow::Node getContainedNode() { result = super.getContainedNode() }
26+
27+
/**
28+
* Gets the polarity of the check.
29+
*
30+
* If the polarity is `false` the check returns `true` if the container does not contain
31+
* the given element.
32+
*/
33+
final boolean getPolarity() { result = super.getPolarity() }
34+
}
35+
36+
/**
37+
* Contains classes for recognizing array and string inclusion tests.
38+
*/
39+
module InclusionTest {
40+
/**
41+
* An expression that is equivalent to `A.include?(B)` or `!A.include?(B)`.
42+
*
43+
* Note that this also includes calls to the array method named `include?`.
44+
*/
45+
abstract class Range extends DataFlow::Node {
46+
/** Gets the `A` in `A.include?(B)`. */
47+
abstract DataFlow::Node getContainerNode();
48+
49+
/** Gets the `B` in `A.include?(B)`. */
50+
abstract DataFlow::Node getContainedNode();
51+
52+
/**
53+
* Gets the polarity of the check.
54+
*
55+
* If the polarity is `false` the check returns `true` if the container does not contain
56+
* the given element.
57+
*/
58+
boolean getPolarity() { result = true }
59+
}
60+
61+
/**
62+
* A call to a method named `include?`, assumed to refer to `String.include?`
63+
* or `Array.include?`.
64+
*/
65+
private class Includes_Native extends Range, DataFlow::CallNode {
66+
Includes_Native() {
67+
this.getMethodName() = "include?" and
68+
strictcount(this.getArgument(_)) = 1
69+
}
70+
71+
override DataFlow::Node getContainerNode() { result = this.getReceiver() }
72+
73+
override DataFlow::Node getContainedNode() { result = this.getArgument(0) }
74+
}
75+
76+
/**
77+
* A check of form `A.index(B) != nil`, `A.index(B) >= 0`, or similar.
78+
*/
79+
private class Includes_IndexOfComparison extends Range, DataFlow::Node {
80+
private DataFlow::CallNode indexOf;
81+
private boolean polarity;
82+
83+
Includes_IndexOfComparison() {
84+
exists(ExprCfgNode index, ExprNodes::ComparisonOperationCfgNode comparison, int value |
85+
indexOf.asExpr() = comparison.getAnOperand() and
86+
index = comparison.getAnOperand() and
87+
this.asExpr() = comparison and
88+
// one operand is of the form `whitelist.index(x)`
89+
indexOf.getMethodName() = "index" and
90+
// and the other one is 0 or -1
91+
(
92+
value = index.getConstantValue().getInt() and value = 0
93+
or
94+
index.getConstantValue().isNil() and value = -1
95+
)
96+
|
97+
value = -1 and polarity = false and comparison.getExpr() instanceof CaseEqExpr
98+
or
99+
value = -1 and polarity = false and comparison.getExpr() instanceof EqExpr
100+
or
101+
value = -1 and polarity = true and comparison.getExpr() instanceof NEExpr
102+
or
103+
exists(RelationalOperation op | op = comparison.getExpr() |
104+
exists(Expr lesser, Expr greater |
105+
op.getLesserOperand() = lesser and
106+
op.getGreaterOperand() = greater
107+
|
108+
polarity = true and
109+
greater = indexOf.asExpr().getExpr() and
110+
(
111+
value = 0 and op.isInclusive()
112+
or
113+
value = -1 and not op.isInclusive()
114+
)
115+
or
116+
polarity = false and
117+
lesser = indexOf.asExpr().getExpr() and
118+
(
119+
value = -1 and op.isInclusive()
120+
or
121+
value = 0 and not op.isInclusive()
122+
)
123+
)
124+
)
125+
)
126+
}
127+
128+
override DataFlow::Node getContainerNode() { result = indexOf.getReceiver() }
129+
130+
override DataFlow::Node getContainedNode() { result = indexOf.getArgument(0) }
131+
132+
override boolean getPolarity() { result = polarity }
133+
}
134+
}

0 commit comments

Comments
 (0)