Skip to content

Commit b3691cd

Browse files
committed
JS: change MembershipTest to MembershipCandidate
1 parent ddb545c commit b3691cd

File tree

9 files changed

+276
-276
lines changed

9 files changed

+276
-276
lines changed

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,12 @@ DataFlow::Node schemeCheck(DataFlow::Node nd, DangerousScheme scheme) {
6161
sw.getSubstring().mayHaveStringValue(scheme)
6262
)
6363
or
64-
exists(DataFlow::Node candidate, MembershipTest t |
65-
result = t and
66-
t.getCandidate() = candidate and
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+
|
6770
t.getAMemberString() = scheme.getWithOrWithoutColon() and
6871
schemeOf(nd).flowsTo(candidate)
6972
)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ class WhitelistInclusionGuard extends DataFlow::LabeledBarrierGuardNode {
452452
this instanceof TaintTracking::PositiveIndexOfSanitizer
453453
or
454454
this instanceof TaintTracking::MembershipTestSanitizer and
455-
not this instanceof MembershipTest::ObjectPropertyNameMembershipTest // handled with more precision in `HasOwnPropertyGuard`
455+
not this = any(MembershipCandidate::ObjectPropertyNameMembershipCandidate c).getTest() // handled with more precision in `HasOwnPropertyGuard`
456456
}
457457

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

javascript/ql/src/javascript.qll

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

0 commit comments

Comments
 (0)