Skip to content

Commit ddb545c

Browse files
committed
JS: introduce MembershipTests.qll and use in two locations
1 parent 6041d52 commit ddb545c

File tree

13 files changed

+395
-26
lines changed

13 files changed

+395
-26
lines changed

change-notes/1.25/analysis-javascript.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
- [marsdb](https://www.npmjs.com/package/marsdb)
1111
- [minimongo](https://www.npmjs.com/package/minimongo/)
1212

13+
* The analysis of sanitizers has improved, leading to more accurate
14+
results from the security queries.
15+
1316
## New queries
1417

1518
| **Query** | **Tags** | **Purpose** |

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

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

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -825,18 +825,22 @@ module TaintTracking {
825825
override predicate appliesTo(Configuration cfg) { any() }
826826
}
827827

828-
/** DEPRECATED. This class has been renamed to `InclusionSanitizer`. */
829-
deprecated class StringInclusionSanitizer = InclusionSanitizer;
828+
/** DEPRECATED. This class has been renamed to `MembershipTestSanitizer`. */
829+
deprecated class StringInclusionSanitizer = MembershipTestSanitizer;
830830

831-
/** A check of the form `whitelist.includes(x)` or equivalent, which sanitizes `x` in its "then" branch. */
832-
class InclusionSanitizer extends AdditionalSanitizerGuardNode {
833-
InclusionTest inclusion;
831+
/** DEPRECATED. This class has been renamed to `MembershipTestSanitizer`. */
832+
deprecated class InclusionSanitizer = MembershipTestSanitizer;
834833

835-
InclusionSanitizer() { this = inclusion }
834+
/**
835+
* A check of the form `whitelist.includes(x)` or equivalent, which sanitizes `x` in its "then" branch.
836+
*/
837+
class MembershipTestSanitizer extends AdditionalSanitizerGuardNode {
838+
MembershipTest test;
839+
840+
MembershipTestSanitizer() { this = test }
836841

837842
override predicate sanitizes(boolean outcome, Expr e) {
838-
outcome = inclusion.getPolarity() and
839-
e = inclusion.getContainedNode().asExpr()
843+
test.getCandidate() = e.flow() and test.getPolarity() = outcome
840844
}
841845

842846
override predicate appliesTo(Configuration cfg) { any() }
@@ -871,8 +875,12 @@ module TaintTracking {
871875
/** Gets a variable that is defined exactly once. */
872876
private Variable singleDef() { strictcount(result.getADefinition()) = 1 }
873877

874-
/** A check of the form `if(x == 'some-constant')`, which sanitizes `x` in its "then" branch. */
875-
class ConstantComparison extends AdditionalSanitizerGuardNode, DataFlow::ValueNode {
878+
/**
879+
* A check of the form `if(x == 'some-constant')`, which sanitizes `x` in its "then" branch.
880+
*
881+
* DEPRECATED: use `MembershipTests::MembershipTest` instead.
882+
*/
883+
deprecated class ConstantComparison extends SanitizerGuardNode, DataFlow::ValueNode {
876884
Expr x;
877885
override EqualityTest astNode;
878886

@@ -890,7 +898,10 @@ module TaintTracking {
890898
outcome = astNode.getPolarity() and x = e
891899
}
892900

893-
override predicate appliesTo(Configuration cfg) { any() }
901+
/**
902+
* Holds if this guard applies to the flow in `cfg`.
903+
*/
904+
predicate appliesTo(Configuration cfg) { any() }
894905
}
895906

896907
/**

0 commit comments

Comments
 (0)