Skip to content

Commit e7800d4

Browse files
authored
Merge pull request github#3415 from esbena/js/membershiptest
Approved by asgerf
2 parents b099f13 + f9ed64f commit e7800d4

File tree

16 files changed

+399
-51
lines changed

16 files changed

+399
-51
lines changed

change-notes/1.25/analysis-javascript.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323

2424
* TypeScript 3.9 is now supported.
2525

26+
* The analysis of sanitizers has improved, leading to more accurate
27+
results from the security queries.
28+
2629
## New queries
2730

2831
| **Query** | **Tags** | **Purpose** |

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

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,18 +61,14 @@ DataFlow::Node schemeCheck(DataFlow::Node nd, DangerousScheme scheme) {
6161
sw.getSubstring().mayHaveStringValue(scheme)
6262
)
6363
or
64-
// check of the form `array.includes(getScheme(nd))`
65-
exists(InclusionTest test, DataFlow::ArrayCreationNode array | test = result |
66-
schemeOf(nd).flowsTo(test.getContainedNode()) and
67-
array.flowsTo(test.getContainerNode()) and
68-
array.getAnElement().mayHaveStringValue(scheme.getWithOrWithoutColon())
69-
)
70-
or
71-
// check of the form `getScheme(nd) === scheme`
72-
exists(EqualityTest test, Expr op1, Expr op2 | test.flow() = result |
73-
test.hasOperands(op1, op2) and
74-
schemeOf(nd).flowsToExpr(op1) and
75-
op2.mayHaveStringValue(scheme.getWithOrWithoutColon())
64+
exists(MembershipCandidate candidate |
65+
result = candidate.getTest()
66+
or
67+
// fall back to the candidate if the test itself is implicit
68+
not exists(candidate.getTest()) and result = candidate
69+
|
70+
candidate.getAMemberString() = scheme.getWithOrWithoutColon() and
71+
schemeOf(nd).flowsTo(candidate)
7672
)
7773
or
7874
// propagate through trimming, case conversion, and regexp replace

javascript/ql/src/Security/CWE-400/PrototypePollutionUtility.ql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,10 @@ class BlacklistInclusionGuard extends DataFlow::LabeledBarrierGuardNode, Inclusi
449449
*/
450450
class WhitelistInclusionGuard extends DataFlow::LabeledBarrierGuardNode {
451451
WhitelistInclusionGuard() {
452-
this instanceof TaintTracking::PositiveIndexOfSanitizer or
453-
this instanceof TaintTracking::InclusionSanitizer
452+
this instanceof TaintTracking::PositiveIndexOfSanitizer
453+
or
454+
this instanceof TaintTracking::MembershipTestSanitizer and
455+
not this = any(MembershipCandidate::ObjectPropertyNameMembershipCandidate c).getTest() // handled with more precision in `HasOwnPropertyGuard`
454456
}
455457

456458
override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel lbl) {

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import semmle.javascript.JsonParsers
3737
import semmle.javascript.JSX
3838
import semmle.javascript.Lines
3939
import semmle.javascript.Locations
40+
import semmle.javascript.MembershipCandidates
4041
import semmle.javascript.Modules
4142
import semmle.javascript.NodeJS
4243
import semmle.javascript.NPM

javascript/ql/src/semmle/javascript/InclusionTests.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
private import javascript
66

77
/**
8-
* A expression that checks if an element is contained in an array
8+
* An expression that checks if an element is contained in an array
99
* or is a substring of another string.
1010
*
1111
* Examples:
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/**
2+
* Provides classes for recognizing membership tests.
3+
*/
4+
5+
import javascript
6+
7+
/**
8+
* An expression that is tested for membership of a collection.
9+
*
10+
* Additional candidates can be added by subclassing `MembershipCandidate::Range`
11+
*/
12+
class MembershipCandidate extends DataFlow::Node {
13+
MembershipCandidate::Range range;
14+
15+
MembershipCandidate() { this = range }
16+
17+
/**
18+
* Gets the expression that performs the membership test, if any.
19+
*/
20+
DataFlow::Node getTest() { result = range.getTest() }
21+
22+
/**
23+
* Gets a string that this candidate is tested against, if
24+
* it can be determined.
25+
*/
26+
string getAMemberString() { result = range.getAMemberString() }
27+
28+
/**
29+
* Gets a node that this candidate is tested against, if
30+
* it can be determined.
31+
*/
32+
DataFlow::Node getAMemberNode() { result = range.getAMemberNode() }
33+
34+
/**
35+
* Gets the polarity of the test.
36+
*
37+
* If the polarity is `false` the test returns `true` if the
38+
* collection does not contain this candidate.
39+
*/
40+
boolean getTestPolarity() { result = range.getTestPolarity() }
41+
}
42+
43+
/**
44+
* Provides classes for recognizing membership candidates.
45+
*/
46+
module MembershipCandidate {
47+
/**
48+
* An expression that is tested for membership of a collection.
49+
*/
50+
abstract class Range extends DataFlow::Node {
51+
/**
52+
* Gets the expression that performs the membership test, if any.
53+
*/
54+
abstract DataFlow::Node getTest();
55+
56+
/**
57+
* Gets a string that this candidate is tested against, if
58+
* it can be determined.
59+
*/
60+
string getAMemberString() { this.getAMemberNode().mayHaveStringValue(result) }
61+
62+
/**
63+
* Gets a node that this candidate is tested against, if
64+
* it can be determined.
65+
*/
66+
DataFlow::Node getAMemberNode() { none() }
67+
68+
/**
69+
* Gets the polarity of the test.
70+
*
71+
* If the polarity is `false` the test returns `true` if the
72+
* collection does not contain this candidate.
73+
*/
74+
boolean getTestPolarity() { result = true }
75+
}
76+
77+
/**
78+
* An `InclusionTest` candidate viewed as a `MembershipCandidate`.
79+
*/
80+
private class OrdinaryInclusionTestCandidate extends MembershipCandidate::Range {
81+
InclusionTest test;
82+
83+
OrdinaryInclusionTestCandidate() { this = test.getContainedNode() }
84+
85+
override DataFlow::Node getTest() { result = test }
86+
87+
override boolean getTestPolarity() { result = test.getPolarity() }
88+
}
89+
90+
/**
91+
* A candidate that may be a member of an array.
92+
*/
93+
class ArrayMembershipCandidate extends OrdinaryInclusionTestCandidate {
94+
DataFlow::ArrayCreationNode array;
95+
96+
ArrayMembershipCandidate() { array.flowsTo(test.getContainerNode()) }
97+
98+
/**
99+
* Gets the array of this test.
100+
*/
101+
DataFlow::ArrayCreationNode getArray() { result = array }
102+
103+
override DataFlow::Node getAMemberNode() { result = array.getAnElement() }
104+
}
105+
106+
/**
107+
* A candidate that may be a member of an array constructed
108+
* from a call to `String.prototype.split`.
109+
*/
110+
private class ShorthandArrayMembershipCandidate extends OrdinaryInclusionTestCandidate {
111+
string toSplit;
112+
string separator;
113+
114+
ShorthandArrayMembershipCandidate() {
115+
exists(StringSplitCall split |
116+
split.flowsTo(test.getContainerNode()) and
117+
toSplit = split.getBaseString().getStringValue() and
118+
separator = split.getSeparator()
119+
)
120+
}
121+
122+
override string getAMemberString() { result = toSplit.splitAt(separator) }
123+
}
124+
125+
/**
126+
* An `EqualityTest` operand viewed as a `MembershipCandidate`.
127+
*/
128+
private class EqualityTestOperand extends MembershipCandidate::Range, DataFlow::ValueNode {
129+
EqualityTest test;
130+
131+
EqualityTestOperand() { this = test.getAnOperand().flow() }
132+
133+
override DataFlow::Node getTest() { result = test.flow() }
134+
135+
override DataFlow::Node getAMemberNode() { test.hasOperands(this.asExpr(), result.asExpr()) }
136+
137+
override boolean getTestPolarity() { result = test.getPolarity() }
138+
}
139+
140+
/**
141+
* A regular expression that enumerates all of its matched strings.
142+
*/
143+
private class EnumerationRegExp extends RegExpTerm {
144+
EnumerationRegExp() {
145+
this.isRootTerm() and
146+
RegExp::isFullyAnchoredTerm(this) and
147+
exists(RegExpTerm child | this.getAChild*() = child |
148+
child instanceof RegExpSequence or
149+
child instanceof RegExpCaret or
150+
child instanceof RegExpDollar or
151+
child instanceof RegExpConstant or
152+
child instanceof RegExpAlt or
153+
child instanceof RegExpGroup
154+
)
155+
}
156+
157+
/**
158+
* Gets a string matched by this regular expression.
159+
*/
160+
string getAMember() { result = this.getAChild*().getAMatchedString() }
161+
}
162+
163+
/**
164+
* A candidate that may be matched by a regular expression that
165+
* enumerates all of its matched strings.
166+
*/
167+
private class RegExpEnumerationCandidate extends MembershipCandidate::Range, DataFlow::Node {
168+
DataFlow::Node test;
169+
EnumerationRegExp enumeration;
170+
boolean polarity;
171+
172+
RegExpEnumerationCandidate() {
173+
exists(DataFlow::MethodCallNode mcn, DataFlow::Node base, string m, DataFlow::Node firstArg |
174+
(
175+
test = any(ConditionGuardNode g).getTest().flow() and
176+
mcn.flowsTo(test) and
177+
polarity = true
178+
or
179+
exists(EqualityTest eq, Expr null, Expr op |
180+
test = eq.flow() and
181+
polarity = eq.getPolarity().booleanNot() and
182+
mcn.flowsToExpr(op) and
183+
eq.hasOperands(op, null) and
184+
SyntacticConstants::isNull(null)
185+
)
186+
) and
187+
mcn.calls(base, m) and
188+
firstArg = mcn.getArgument(0)
189+
|
190+
// /re/.test(u) or /re/.exec(u)
191+
enumeration = RegExp::getRegExpObjectFromNode(base) and
192+
(m = "test" or m = "exec") and
193+
firstArg = this
194+
or
195+
// u.match(/re/) or u.match("re")
196+
base = this and
197+
m = "match" and
198+
enumeration = RegExp::getRegExpFromNode(firstArg)
199+
)
200+
}
201+
202+
override DataFlow::Node getTest() { result = test }
203+
204+
override string getAMemberString() { result = enumeration.getAMember() }
205+
206+
override boolean getTestPolarity() { result = polarity }
207+
}
208+
209+
/**
210+
* A candidate that may be a member of a collection class, such as a map or set.
211+
*/
212+
class CollectionMembershipCandidate extends MembershipCandidate::Range {
213+
DataFlow::MethodCallNode test;
214+
215+
CollectionMembershipCandidate() { test.getMethodName() = "has" and this = test.getArgument(0) }
216+
217+
override DataFlow::Node getTest() { result = test }
218+
}
219+
220+
/**
221+
* A candidate that may be a property name of an object.
222+
*/
223+
class ObjectPropertyNameMembershipCandidate extends MembershipCandidate::Range,
224+
DataFlow::ValueNode {
225+
DataFlow::ValueNode test;
226+
DataFlow::ValueNode membersNode;
227+
228+
ObjectPropertyNameMembershipCandidate() {
229+
exists(InExpr inExpr |
230+
this = inExpr.getLeftOperand().flow() and
231+
test = inExpr.flow() and
232+
membersNode = inExpr.getRightOperand().flow()
233+
)
234+
or
235+
exists(DataFlow::MethodCallNode hasOwn |
236+
this = hasOwn.getArgument(0) and
237+
test = hasOwn and
238+
hasOwn.calls(membersNode, "hasOwnProperty")
239+
)
240+
}
241+
242+
override DataFlow::Node getTest() { result = test }
243+
244+
override string getAMemberString() {
245+
exists(membersNode.getALocalSource().getAPropertyWrite(result))
246+
}
247+
}
248+
249+
/**
250+
* A candidate that may match a switch case.
251+
*/
252+
class SwitchCaseCandidate extends MembershipCandidate::Range {
253+
SwitchStmt switch;
254+
255+
SwitchCaseCandidate() { this = switch.getExpr().flow() }
256+
257+
override DataFlow::Node getTest() { none() }
258+
259+
override DataFlow::Node getAMemberNode() { result = switch.getACase().getExpr().flow() }
260+
}
261+
}

javascript/ql/src/semmle/javascript/dataflow/TaintTracking.qll

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,9 @@ module TaintTracking {
827827
override predicate appliesTo(Configuration cfg) { any() }
828828
}
829829

830+
/** DEPRECATED. This class has been renamed to `MembershipTestSanitizer`. */
831+
deprecated class StringInclusionSanitizer = MembershipTestSanitizer;
832+
830833
/**
831834
* A test of form `x.length === "0"`, preventing `x` from being tainted.
832835
*/
@@ -849,18 +852,19 @@ module TaintTracking {
849852
override predicate appliesTo(Configuration cfg) { any() }
850853
}
851854

852-
/** DEPRECATED. This class has been renamed to `InclusionSanitizer`. */
853-
deprecated class StringInclusionSanitizer = InclusionSanitizer;
855+
/** DEPRECATED. This class has been renamed to `MembershipTestSanitizer`. */
856+
deprecated class InclusionSanitizer = MembershipTestSanitizer;
854857

855-
/** A check of the form `whitelist.includes(x)` or equivalent, which sanitizes `x` in its "then" branch. */
856-
class InclusionSanitizer extends AdditionalSanitizerGuardNode {
857-
InclusionTest inclusion;
858+
/**
859+
* A check of the form `whitelist.includes(x)` or equivalent, which sanitizes `x` in its "then" branch.
860+
*/
861+
class MembershipTestSanitizer extends AdditionalSanitizerGuardNode {
862+
MembershipCandidate candidate;
858863

859-
InclusionSanitizer() { this = inclusion }
864+
MembershipTestSanitizer() { this = candidate.getTest() }
860865

861866
override predicate sanitizes(boolean outcome, Expr e) {
862-
outcome = inclusion.getPolarity() and
863-
e = inclusion.getContainedNode().asExpr()
867+
candidate = e.flow() and candidate.getTestPolarity() = outcome
864868
}
865869

866870
override predicate appliesTo(Configuration cfg) { any() }
@@ -895,8 +899,12 @@ module TaintTracking {
895899
/** Gets a variable that is defined exactly once. */
896900
private Variable singleDef() { strictcount(result.getADefinition()) = 1 }
897901

898-
/** A check of the form `if(x == 'some-constant')`, which sanitizes `x` in its "then" branch. */
899-
class ConstantComparison extends AdditionalSanitizerGuardNode, DataFlow::ValueNode {
902+
/**
903+
* A check of the form `if(x == 'some-constant')`, which sanitizes `x` in its "then" branch.
904+
*
905+
* DEPRECATED: use `MembershipTestSanitizer` instead.
906+
*/
907+
deprecated class ConstantComparison extends SanitizerGuardNode, DataFlow::ValueNode {
900908
Expr x;
901909
override EqualityTest astNode;
902910

@@ -914,7 +922,10 @@ module TaintTracking {
914922
outcome = astNode.getPolarity() and x = e
915923
}
916924

917-
override predicate appliesTo(Configuration cfg) { any() }
925+
/**
926+
* Holds if this guard applies to the flow in `cfg`.
927+
*/
928+
predicate appliesTo(Configuration cfg) { any() }
918929
}
919930

920931
/**

0 commit comments

Comments
 (0)