Skip to content

Commit 546431c

Browse files
committed
dataflow and typetracking steps for Maps and Sets
1 parent 25aea90 commit 546431c

6 files changed

Lines changed: 425 additions & 2 deletions

File tree

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import semmle.javascript.JsonParsers
3636
import semmle.javascript.JSX
3737
import semmle.javascript.Lines
3838
import semmle.javascript.Locations
39+
import semmle.javascript.MapAndSet
3940
import semmle.javascript.Modules
4041
import semmle.javascript.NodeJS
4142
import semmle.javascript.NPM
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import javascript
2+
private import semmle.javascript.dataflow.internal.StepSummary
3+
private import DataFlow::PseudoProperties
4+
5+
/**
6+
* Common predicates and classes for type and data-flow tracking on Maps and Sets.
7+
*/
8+
private module MapsAndSets {
9+
/**
10+
* An `AdditionalFlowStep` used to model a data-flow step related to Maps and Sets.
11+
*
12+
* The `loadStep`/`storeStep`/`loadStoreStep` methods are overloaded such that the new predicates
13+
* `load`/`store`/`loadStore` can be used in the `MapsAndSetsTypeTracking` module.
14+
* (Thereby avoiding conflicts with a "cousin" `AdditionalFlowStep` implementation.)
15+
*/
16+
abstract class MapOrSetFlowStep extends DataFlow::AdditionalFlowStep {
17+
final override predicate step(DataFlow::Node pred, DataFlow::Node succ) { none() }
18+
19+
final override predicate step(
20+
DataFlow::Node p, DataFlow::Node s, DataFlow::FlowLabel pl, DataFlow::FlowLabel sl
21+
) {
22+
none()
23+
}
24+
25+
/**
26+
* Holds if the property `prop` of the object `pred` should be loaded into `succ`.
27+
*/
28+
predicate load(DataFlow::Node pred, DataFlow::Node succ, string prop) { none() }
29+
30+
final override predicate loadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
31+
this.load(pred, succ, prop)
32+
}
33+
34+
/**
35+
* Holds if `pred` should be stored in the object `succ` under the property `prop`.
36+
*/
37+
predicate store(DataFlow::Node pred, DataFlow::Node succ, string prop) { none() }
38+
39+
final override predicate storeStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
40+
this.store(pred, succ, prop)
41+
}
42+
43+
/**
44+
* Holds if the property `prop` should be copied from the object `pred` to the object `succ`.
45+
*/
46+
predicate loadStore(DataFlow::Node pred, DataFlow::Node succ, string prop) { none() }
47+
48+
final override predicate loadStoreStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
49+
this.loadStore(pred, succ, prop, prop)
50+
}
51+
52+
/**
53+
* Holds if the property `loadProp` should be copied from the object `pred` to the property `storeProp` of object `succ`.
54+
*/
55+
predicate loadStore(DataFlow::Node pred, DataFlow::Node succ, string loadProp, string storeProp) {
56+
none()
57+
}
58+
59+
final override predicate loadStoreStep(
60+
DataFlow::Node pred, DataFlow::Node succ, string loadProp, string storeProp
61+
) {
62+
this.loadStore(pred, succ, loadProp, storeProp)
63+
}
64+
65+
/**
66+
* Holds if this is a map step that could potentially load a value where the corresponding key has a known string value.
67+
*/
68+
predicate canLoadKnownKey() { none() }
69+
}
70+
}
71+
72+
/**
73+
* A collection of predicates and clases for type-tracking Maps and Sets.
74+
*/
75+
module MapsAndSetsTypeTracking {
76+
private import MapsAndSets
77+
78+
/**
79+
* Gets the result from a single step through a Map or Set, from `pred` to `result` summarized by `summary`.
80+
*/
81+
pragma[inline]
82+
DataFlow::SourceNode mapOrSetStep(DataFlow::Node pred, StepSummary summary) {
83+
exists(MapOrSetFlowStep step, string field |
84+
summary = LoadStep(field) and
85+
step.load(pred, result, field) and
86+
(not step.canLoadKnownKey() or not field = mapValueUnknownKey()) // for a step that could load a known key, we prune the steps where the key is unknown.
87+
or
88+
summary = StoreStep(field) and
89+
step.store(pred, result, field)
90+
or
91+
exists(string toField | summary = LoadStoreStep(field, toField) |
92+
field = toField and
93+
step.loadStore(pred, result, field)
94+
or
95+
step.loadStore(pred, result, field, toField)
96+
)
97+
)
98+
}
99+
100+
/**
101+
* Gets the result from a single step through a Map or set, from `pred` with tracker `t2` to `result` with tracker `t`.
102+
*/
103+
pragma[inline]
104+
DataFlow::SourceNode mapOrSetStep(
105+
DataFlow::SourceNode pred, DataFlow::TypeTracker t, DataFlow::TypeTracker t2
106+
) {
107+
exists(DataFlow::Node mid, StepSummary summary | pred.flowsTo(mid) and t = t2.append(summary) |
108+
result = mapOrSetStep(mid, summary)
109+
)
110+
}
111+
112+
/**
113+
* A class enabling the use of the Map and Set related pseudo-properties as a pseudo-property in type-tracking predicates.
114+
*/
115+
private class MapRelatedPseudoFieldAsTypeTrackingProperty extends TypeTrackingPseudoProperty {
116+
MapRelatedPseudoFieldAsTypeTrackingProperty() {
117+
this = [setElement(), iteratorElement()] or
118+
any(MapOrSetFlowStep step).store(_, _, this)
119+
}
120+
121+
override string getLoadStoreToProp() {
122+
exists(MapOrSetFlowStep step | step.loadStore(_, _, this, result))
123+
}
124+
}
125+
}
126+
127+
/**
128+
* A module for data-flow steps related to `Set` and `Map`.
129+
*/
130+
private module MapAndSetDataFlow {
131+
private import MapsAndSets
132+
133+
/**
134+
* A step for an `add` method, which adds an element to a Set.
135+
*/
136+
private class SetAdd extends MapOrSetFlowStep, DataFlow::MethodCallNode {
137+
SetAdd() { this.getMethodName() = "add" }
138+
139+
override predicate store(DataFlow::Node element, DataFlow::Node obj, string prop) {
140+
obj = this.getReceiver().getALocalSource() and
141+
element = this.getArgument(0) and
142+
prop = setElement()
143+
}
144+
}
145+
146+
/**
147+
* A step for the `Set` constructor, which copies any elements from the first argument into the resulting set.
148+
*/
149+
private class SetConstructor extends MapOrSetFlowStep, DataFlow::NewNode {
150+
SetConstructor() { this = DataFlow::globalVarRef("Set").getAnInstantiation() }
151+
152+
override predicate loadStore(
153+
DataFlow::Node pred, DataFlow::Node succ, string fromProp, string toProp
154+
) {
155+
pred = this.getArgument(0) and
156+
succ = this and
157+
fromProp = arrayLikeElement() and
158+
toProp = setElement()
159+
}
160+
}
161+
162+
/**
163+
* A step for a `for of` statement on a Map, Set, or Iterator.
164+
* For Sets and iterators the l-value are the elements of the set/iterator.
165+
* For Maps the l-value is a tuple containing a key and a value.
166+
*
167+
* This is partially duplicated behavior with the `for of` step for Arrays (in Arrays.qll).
168+
* This duplication is required for the type-tracking steps defined in `MapsAndSetsTypeTracking`.
169+
*/
170+
private class ForOfStep extends MapOrSetFlowStep, DataFlow::ValueNode {
171+
ForOfStmt forOf;
172+
DataFlow::Node element;
173+
174+
ForOfStep() {
175+
this.asExpr() = forOf.getIterationDomain() and
176+
element = DataFlow::lvalueNode(forOf.getLValue())
177+
}
178+
179+
override predicate load(DataFlow::Node obj, DataFlow::Node e, string prop) {
180+
obj = this and
181+
e = element and
182+
prop = arrayLikeElement()
183+
}
184+
185+
override predicate loadStore(
186+
DataFlow::Node pred, DataFlow::Node succ, string fromProp, string toProp
187+
) {
188+
pred = this and
189+
succ = element and
190+
fromProp = mapValueUnknownKey() and
191+
toProp = "1"
192+
}
193+
}
194+
195+
/**
196+
* A step for a call to `forEach` on a Set or Map.
197+
*/
198+
private class SetMapForEach extends MapOrSetFlowStep, DataFlow::MethodCallNode {
199+
SetMapForEach() { this.getMethodName() = "forEach" }
200+
201+
override predicate load(DataFlow::Node obj, DataFlow::Node element, string prop) {
202+
obj = this.getReceiver() and
203+
element = this.getCallback(0).getParameter(0) and
204+
prop = [setElement(), mapValueUnknownKey()]
205+
}
206+
}
207+
208+
/**
209+
* A call to the `get` method on a Map.
210+
* If the key of the call to `get` has a known string value, then only the value corresponding to that key will be retrieved.
211+
*/
212+
private class MapGet extends MapOrSetFlowStep, DataFlow::MethodCallNode {
213+
MapGet() { this.getMethodName() = "get" }
214+
215+
override predicate load(DataFlow::Node obj, DataFlow::Node element, string prop) {
216+
obj = this.getReceiver() and
217+
element = this and
218+
prop = mapValue(this.getArgument(0))
219+
}
220+
221+
override predicate canLoadKnownKey() { any() }
222+
}
223+
224+
/**
225+
* A call to the `set` method on a Map.
226+
*
227+
* If the key of the call to `set` has a known string value,
228+
* then the value will be saved into a pseudo-property corresponding to the known string value.
229+
* The value will additionally be saved into a pseudo-property corresponding to values with unknown keys.
230+
*/
231+
private class MapSet extends MapOrSetFlowStep, DataFlow::MethodCallNode {
232+
MapSet() { this.getMethodName() = "set" }
233+
234+
override predicate store(DataFlow::Node element, DataFlow::Node obj, string prop) {
235+
obj = this.getReceiver().getALocalSource() and
236+
element = this.getArgument(1) and
237+
// Makes sure that both known and unknown gets will work.
238+
prop = [mapValue(this.getArgument(0)), mapValueUnknownKey()]
239+
}
240+
}
241+
242+
/**
243+
* A step for a call to `values` on a Map or a Set.
244+
*/
245+
private class MapAndSetValues extends MapOrSetFlowStep, DataFlow::MethodCallNode {
246+
MapAndSetValues() { this.getMethodName() = "values" }
247+
248+
override predicate loadStore(
249+
DataFlow::Node pred, DataFlow::Node succ, string fromProp, string toProp
250+
) {
251+
pred = this.getReceiver() and
252+
succ = this and
253+
fromProp = [mapValueUnknownKey(),setElement()] and
254+
toProp = iteratorElement()
255+
}
256+
}
257+
258+
/**
259+
* A step for a call to `keys` on a Set.
260+
*/
261+
private class SetKeys extends MapOrSetFlowStep, DataFlow::MethodCallNode {
262+
SetKeys() { this.getMethodName() = "keys" }
263+
264+
override predicate loadStore(
265+
DataFlow::Node pred, DataFlow::Node succ, string fromProp, string toProp
266+
) {
267+
pred = this.getReceiver() and
268+
succ = this and
269+
fromProp = setElement() and
270+
toProp = iteratorElement()
271+
}
272+
}
273+
}

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -579,15 +579,50 @@ abstract class AdditionalFlowStep extends DataFlow::Node {
579579
* For use with load/store steps in `DataFlow::AdditionalFlowStep` and TypeTracking.
580580
*/
581581
module PseudoProperties {
582+
/**
583+
* Gets a pseudo-property representing elements inside a `Set`
584+
*/
585+
string setElement() { result = "$setElement$" }
586+
587+
/**
588+
* Gets a pseudo-property representing elements inside a JavaScript iterator.
589+
*/
590+
string iteratorElement() { result = "$iteratorElement$" }
591+
582592
/**
583593
* Gets a pseudo-field representing an elements inside an `Array`.
584594
*/
585595
string arrayElement() { result = "$arrayElement$" }
586596

587597
/**
588-
* Gets a pseudo-property representing elements inside some array-like object.
598+
* Gets a pseudo-property representing elements inside some array-like object. (Set, Array, or Iterator).
599+
*/
600+
string arrayLikeElement() { result = [setElement(), iteratorElement(), arrayElement()] }
601+
602+
/**
603+
* Gets a pseudo-property representing the values of a Map, where the key is unknown.
589604
*/
590-
string arrayLikeElement() { result = arrayElement() }
605+
string mapValueUnknownKey() { result = "$UnknownMapValue$" }
606+
607+
/**
608+
* Gets a pseudo property for a Map value where the key is `key`.
609+
* The string value of the `key` is encoded in the result, and there is only a result if the string value of `key` is known.
610+
*/
611+
pragma[inline]
612+
string mapValueKnownKey(DataFlow::Node key) {
613+
exists(string s | key.mayHaveStringValue(s) | result = "$mapValue|" + s + "$")
614+
}
615+
616+
/**
617+
* Gets a psuedo property for a map value where the key is `key`.
618+
*/
619+
pragma[inline]
620+
string mapValue(DataFlow::Node key) {
621+
result = mapValueKnownKey(key)
622+
or
623+
not exists(mapValueKnownKey(key)) and
624+
result = mapValueUnknownKey()
625+
}
591626
}
592627

593628
/**
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
dataFlow
2+
| tst.js:2:16:2:23 | source() | tst.js:7:7:7:7 | e |
3+
| tst.js:2:16:2:23 | source() | tst.js:11:10:11:10 | e |
4+
| tst.js:2:16:2:23 | source() | tst.js:17:10:17:10 | v |
5+
| tst.js:2:16:2:23 | source() | tst.js:21:10:21:14 | value |
6+
| tst.js:2:16:2:23 | source() | tst.js:26:10:26:14 | value |
7+
| tst.js:2:16:2:23 | source() | tst.js:30:7:30:7 | e |
8+
| tst.js:2:16:2:23 | source() | tst.js:34:7:34:7 | e |
9+
| tst.js:2:16:2:23 | source() | tst.js:38:7:38:7 | e |
10+
| tst.js:2:16:2:23 | source() | tst.js:42:7:42:7 | e |
11+
| tst.js:2:16:2:23 | source() | tst.js:46:7:46:7 | e |
12+
| tst.js:2:16:2:23 | source() | tst.js:50:10:50:10 | e |
13+
| tst.js:2:16:2:23 | source() | tst.js:53:8:53:21 | map.get("key") |
14+
| tst.js:2:16:2:23 | source() | tst.js:55:8:55:28 | map.get ... nKey()) |
15+
typeTracking
16+
| tst.js:2:16:2:23 | source() | tst.js:2:16:2:23 | source() |
17+
| tst.js:2:16:2:23 | source() | tst.js:6:14:6:14 | e |
18+
| tst.js:2:16:2:23 | source() | tst.js:10:15:10:15 | e |
19+
| tst.js:2:16:2:23 | source() | tst.js:16:15:16:15 | v |
20+
| tst.js:2:16:2:23 | source() | tst.js:20:20:20:24 | value |
21+
| tst.js:2:16:2:23 | source() | tst.js:25:14:25:18 | value |
22+
| tst.js:2:16:2:23 | source() | tst.js:29:14:29:14 | e |
23+
| tst.js:2:16:2:23 | source() | tst.js:33:14:33:14 | e |
24+
| tst.js:2:16:2:23 | source() | tst.js:37:14:37:14 | e |
25+
| tst.js:2:16:2:23 | source() | tst.js:45:14:45:14 | e |
26+
| tst.js:2:16:2:23 | source() | tst.js:53:8:53:21 | map.get("key") |
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import javascript
2+
private import semmle.javascript.dataflow.internal.StepSummary
3+
4+
class Config extends DataFlow::Configuration {
5+
Config() { this = "Config" }
6+
7+
override predicate isSource(DataFlow::Node source) { source.(DataFlow::CallNode).getCalleeName() = "source" }
8+
9+
override predicate isSink(DataFlow::Node sink) {
10+
exists(DataFlow::CallNode call | call.getCalleeName() = "sink" | call.getAnArgument() = sink)
11+
}
12+
}
13+
14+
query predicate dataFlow(DataFlow::Node pred, DataFlow::Node succ) {
15+
any(Config c).hasFlow(pred, succ)
16+
}
17+
18+
DataFlow::SourceNode trackSource(DataFlow::TypeTracker t, DataFlow::SourceNode start) {
19+
t.start() and
20+
result.(DataFlow::CallNode).getCalleeName() = "source" and
21+
start = result
22+
or
23+
exists(DataFlow::TypeTracker t2 | t = t2.step(trackSource(t2, start), result))
24+
or
25+
exists(DataFlow::TypeTracker t2 |
26+
result = MapsAndSetsTypeTracking::mapOrSetStep(trackSource(t2, start), t, t2)
27+
)
28+
}
29+
30+
query DataFlow::SourceNode typeTracking(DataFlow::Node start) {
31+
result = trackSource(DataFlow::TypeTracker::end(), start)
32+
}

0 commit comments

Comments
 (0)