Skip to content

Commit 8544850

Browse files
committed
JS: Generalize StringOps::Includes to ::InclusionTest
1 parent aa7a997 commit 8544850

4 files changed

Lines changed: 187 additions & 157 deletions

File tree

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import semmle.javascript.Functions
2828
import semmle.javascript.GlobalAccessPaths
2929
import semmle.javascript.HTML
3030
import semmle.javascript.HtmlSanitizers
31+
import semmle.javascript.InclusionTests
3132
import semmle.javascript.JSDoc
3233
import semmle.javascript.JSON
3334
import semmle.javascript.JsonParsers
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/**
2+
* Contains classes for recognizing array and string inclusion tests.
3+
*/
4+
private import javascript
5+
6+
/**
7+
* A expression that checks if an element is contained in an array
8+
* or is a substring of another string.
9+
*
10+
* Examples:
11+
* ```
12+
* A.includes(B)
13+
* A.indexOf(B) !== -1
14+
* A.indexOf(B) >= 0
15+
* ~A.indexOf(B)
16+
* ```
17+
*/
18+
class InclusionTest extends DataFlow::Node {
19+
InclusionTest::Range range;
20+
21+
InclusionTest() { this = range }
22+
23+
/** Gets the `A` in `A.includes(B)`. */
24+
DataFlow::Node getContainerNode() { result = range.getContainerNode() }
25+
26+
/** Gets the `B` in `A.includes(B)`. */
27+
DataFlow::Node getContainedNode() { result = range.getContainedNode() }
28+
29+
/**
30+
* Gets the polarity of the check.
31+
*
32+
* If the polarity is `false` the check returns `true` if the container does not contain
33+
* the given element.
34+
*/
35+
boolean getPolarity() { result = range.getPolarity() }
36+
}
37+
38+
module InclusionTest {
39+
/**
40+
* A expression that is equivalent to `A.includes(B)` or `!A.includes(B)`.
41+
*
42+
* Note that this also includes calls to the array method named `includes`.
43+
*/
44+
abstract class Range extends DataFlow::Node {
45+
/** Gets the `A` in `A.includes(B)`. */
46+
abstract DataFlow::Node getContainerNode();
47+
48+
/** Gets the `B` in `A.includes(B)`. */
49+
abstract DataFlow::Node getContainedNode();
50+
51+
/**
52+
* Gets the polarity of the check.
53+
*
54+
* If the polarity is `false` the check returns `true` if the container does not contain
55+
* the given element.
56+
*/
57+
boolean getPolarity() { result = true }
58+
}
59+
60+
/**
61+
* A call to a method named `includes`, assumed to refer to `String.prototype.includes`
62+
* or `Array.prototype.includes`.
63+
*/
64+
private class Includes_Native extends Range, DataFlow::MethodCallNode {
65+
Includes_Native() {
66+
getMethodName() = "includes" and
67+
getNumArgument() = 1
68+
}
69+
70+
override DataFlow::Node getContainerNode() { result = getReceiver() }
71+
72+
override DataFlow::Node getContainedNode() { result = getArgument(0) }
73+
}
74+
75+
/**
76+
* A call to `_.includes` or similar, assumed to operate on strings.
77+
*/
78+
private class Includes_Library extends Range, DataFlow::CallNode {
79+
Includes_Library() {
80+
exists(string name |
81+
this = LodashUnderscore::member(name).getACall() and
82+
(name = "includes" or name = "include" or name = "contains")
83+
or
84+
this = Closure::moduleImport("goog.string." + name).getACall() and
85+
(name = "contains" or name = "caseInsensitiveContains")
86+
)
87+
}
88+
89+
override DataFlow::Node getContainerNode() { result = getArgument(0) }
90+
91+
override DataFlow::Node getContainedNode() { result = getArgument(1) }
92+
}
93+
94+
/**
95+
* A check of form `A.indexOf(B) !== -1` or similar.
96+
*/
97+
private class Includes_IndexOfEquals extends Range, DataFlow::ValueNode {
98+
MethodCallExpr indexOf;
99+
override EqualityTest astNode;
100+
101+
Includes_IndexOfEquals() {
102+
exists(Expr index | astNode.hasOperands(indexOf, index) |
103+
// one operand is of the form `whitelist.indexOf(x)`
104+
indexOf.getMethodName() = "indexOf" and
105+
// and the other one is -1
106+
index.getIntValue() = -1
107+
)
108+
}
109+
110+
override DataFlow::Node getContainerNode() { result = indexOf.getReceiver().flow() }
111+
112+
override DataFlow::Node getContainedNode() { result = indexOf.getArgument(0).flow() }
113+
114+
override boolean getPolarity() { result = astNode.getPolarity().booleanNot() }
115+
}
116+
117+
/**
118+
* A check of form `A.indexOf(B) >= 0` or similar.
119+
*/
120+
private class Includes_IndexOfRelational extends Range, DataFlow::ValueNode {
121+
MethodCallExpr indexOf;
122+
override RelationalComparison astNode;
123+
boolean polarity;
124+
125+
Includes_IndexOfRelational() {
126+
exists(Expr lesser, Expr greater |
127+
astNode.getLesserOperand() = lesser and
128+
astNode.getGreaterOperand() = greater and
129+
indexOf.getMethodName() = "indexOf" and
130+
indexOf.getNumArgument() = 1
131+
|
132+
polarity = true and
133+
greater = indexOf and
134+
(
135+
lesser.getIntValue() = 0 and astNode.isInclusive()
136+
or
137+
lesser.getIntValue() = -1 and not astNode.isInclusive()
138+
)
139+
or
140+
polarity = false and
141+
lesser = indexOf and
142+
(
143+
greater.getIntValue() = -1 and astNode.isInclusive()
144+
or
145+
greater.getIntValue() = 0 and not astNode.isInclusive()
146+
)
147+
)
148+
}
149+
150+
override DataFlow::Node getContainerNode() { result = indexOf.getReceiver().flow() }
151+
152+
override DataFlow::Node getContainedNode() { result = indexOf.getArgument(0).flow() }
153+
154+
override boolean getPolarity() { result = polarity }
155+
}
156+
157+
/**
158+
* An expression of form `~A.indexOf(B)` which, when coerced to a boolean, is equivalent to `A.includes(B)`.
159+
*/
160+
private class Includes_IndexOfBitwise extends Range, DataFlow::ValueNode {
161+
MethodCallExpr indexOf;
162+
override BitNotExpr astNode;
163+
164+
Includes_IndexOfBitwise() {
165+
astNode.getOperand() = indexOf and
166+
indexOf.getMethodName() = "indexOf"
167+
}
168+
169+
override DataFlow::Node getContainerNode() { result = indexOf.getReceiver().flow() }
170+
171+
override DataFlow::Node getContainedNode() { result = indexOf.getArgument(0).flow() }
172+
}
173+
}

javascript/ql/src/semmle/javascript/StringOps.qll

Lines changed: 5 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -185,162 +185,15 @@ module StringOps {
185185
/**
186186
* A expression that is equivalent to `A.includes(B)` or `!A.includes(B)`.
187187
*
188-
* Note that this also includes calls to the array method named `includes`.
188+
* Note that this class is equivalent to `InclusionTest`, which also matches
189+
* inclusion tests on array objects.
189190
*/
190-
class Includes extends DataFlow::Node {
191-
Includes::Range range;
192-
193-
Includes() { this = range }
194-
191+
class Includes extends InclusionTest {
195192
/** Gets the `A` in `A.includes(B)`. */
196-
DataFlow::Node getBaseString() { result = range.getBaseString() }
193+
DataFlow::Node getBaseString() { result = getContainerNode() }
197194

198195
/** Gets the `B` in `A.includes(B)`. */
199-
DataFlow::Node getSubstring() { result = range.getSubstring() }
200-
201-
/**
202-
* Gets the polarity of the check.
203-
*
204-
* If the polarity is `false` the check returns `true` if the string does not contain
205-
* the given substring.
206-
*/
207-
boolean getPolarity() { result = range.getPolarity() }
208-
}
209-
210-
module Includes {
211-
/**
212-
* A expression that is equivalent to `A.includes(B)` or `!A.includes(B)`.
213-
*
214-
* Note that this also includes calls to the array method named `includes`.
215-
*/
216-
abstract class Range extends DataFlow::Node {
217-
/** Gets the `A` in `A.includes(B)`. */
218-
abstract DataFlow::Node getBaseString();
219-
220-
/** Gets the `B` in `A.includes(B)`. */
221-
abstract DataFlow::Node getSubstring();
222-
223-
/**
224-
* Gets the polarity of the check.
225-
*
226-
* If the polarity is `false` the check returns `true` if the string does not contain
227-
* the given substring.
228-
*/
229-
boolean getPolarity() { result = true }
230-
}
231-
232-
/**
233-
* A call to a method named `includes`, assumed to refer to `String.prototype.includes`.
234-
*/
235-
private class Includes_Native extends Range, DataFlow::MethodCallNode {
236-
Includes_Native() {
237-
getMethodName() = "includes" and
238-
getNumArgument() = 1
239-
}
240-
241-
override DataFlow::Node getBaseString() { result = getReceiver() }
242-
243-
override DataFlow::Node getSubstring() { result = getArgument(0) }
244-
}
245-
246-
/**
247-
* A call to `_.includes` or similar, assumed to operate on strings.
248-
*/
249-
private class Includes_Library extends Range, DataFlow::CallNode {
250-
Includes_Library() {
251-
exists(string name |
252-
this = LodashUnderscore::member(name).getACall() and
253-
(name = "includes" or name = "include" or name = "contains")
254-
or
255-
this = Closure::moduleImport("goog.string." + name).getACall() and
256-
(name = "contains" or name = "caseInsensitiveContains")
257-
)
258-
}
259-
260-
override DataFlow::Node getBaseString() { result = getArgument(0) }
261-
262-
override DataFlow::Node getSubstring() { result = getArgument(1) }
263-
}
264-
265-
/**
266-
* A check of form `A.indexOf(B) !== -1` or similar.
267-
*/
268-
private class Includes_IndexOfEquals extends Range, DataFlow::ValueNode {
269-
MethodCallExpr indexOf;
270-
override EqualityTest astNode;
271-
272-
Includes_IndexOfEquals() {
273-
exists(Expr index | astNode.hasOperands(indexOf, index) |
274-
// one operand is of the form `whitelist.indexOf(x)`
275-
indexOf.getMethodName() = "indexOf" and
276-
// and the other one is -1
277-
index.getIntValue() = -1
278-
)
279-
}
280-
281-
override DataFlow::Node getBaseString() { result = indexOf.getReceiver().flow() }
282-
283-
override DataFlow::Node getSubstring() { result = indexOf.getArgument(0).flow() }
284-
285-
override boolean getPolarity() { result = astNode.getPolarity().booleanNot() }
286-
}
287-
288-
/**
289-
* A check of form `A.indexOf(B) >= 0` or similar.
290-
*/
291-
private class Includes_IndexOfRelational extends Range, DataFlow::ValueNode {
292-
MethodCallExpr indexOf;
293-
override RelationalComparison astNode;
294-
boolean polarity;
295-
296-
Includes_IndexOfRelational() {
297-
exists(Expr lesser, Expr greater |
298-
astNode.getLesserOperand() = lesser and
299-
astNode.getGreaterOperand() = greater and
300-
indexOf.getMethodName() = "indexOf" and
301-
indexOf.getNumArgument() = 1
302-
|
303-
polarity = true and
304-
greater = indexOf and
305-
(
306-
lesser.getIntValue() = 0 and astNode.isInclusive()
307-
or
308-
lesser.getIntValue() = -1 and not astNode.isInclusive()
309-
)
310-
or
311-
polarity = false and
312-
lesser = indexOf and
313-
(
314-
greater.getIntValue() = -1 and astNode.isInclusive()
315-
or
316-
greater.getIntValue() = 0 and not astNode.isInclusive()
317-
)
318-
)
319-
}
320-
321-
override DataFlow::Node getBaseString() { result = indexOf.getReceiver().flow() }
322-
323-
override DataFlow::Node getSubstring() { result = indexOf.getArgument(0).flow() }
324-
325-
override boolean getPolarity() { result = polarity }
326-
}
327-
328-
/**
329-
* An expression of form `~A.indexOf(B)` which, when coerced to a boolean, is equivalent to `A.includes(B)`.
330-
*/
331-
private class Includes_IndexOfBitwise extends Range, DataFlow::ValueNode {
332-
MethodCallExpr indexOf;
333-
override BitNotExpr astNode;
334-
335-
Includes_IndexOfBitwise() {
336-
astNode.getOperand() = indexOf and
337-
indexOf.getMethodName() = "indexOf"
338-
}
339-
340-
override DataFlow::Node getBaseString() { result = indexOf.getReceiver().flow() }
341-
342-
override DataFlow::Node getSubstring() { result = indexOf.getArgument(0).flow() }
343-
}
196+
DataFlow::Node getSubstring() { result = getContainedNode() }
344197
}
345198

346199
/**

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -781,15 +781,18 @@ module TaintTracking {
781781
override predicate appliesTo(Configuration cfg) { any() }
782782
}
783783

784+
/** DEPRECATED. This class has been renamed to `InclusionSanitizer`. */
785+
deprecated class StringInclusionSanitizer = InclusionSanitizer;
786+
784787
/** A check of the form `whitelist.includes(x)` or equivalent, which sanitizes `x` in its "then" branch. */
785-
class StringInclusionSanitizer extends AdditionalSanitizerGuardNode {
786-
StringOps::Includes includes;
788+
class InclusionSanitizer extends AdditionalSanitizerGuardNode {
789+
InclusionTest inclusion;
787790

788-
StringInclusionSanitizer() { this = includes }
791+
InclusionSanitizer() { this = inclusion }
789792

790793
override predicate sanitizes(boolean outcome, Expr e) {
791-
outcome = includes.getPolarity() and
792-
e = includes.getSubstring().asExpr()
794+
outcome = inclusion.getPolarity() and
795+
e = inclusion.getContainedNode().asExpr()
793796
}
794797

795798
override predicate appliesTo(Configuration cfg) { any() }

0 commit comments

Comments
 (0)