Skip to content

Commit e3ab94f

Browse files
authored
Merge pull request github#5498 from asgerf/js/flow-through-accessors
Approved by erik-krogh, max-schaefer
2 parents 12a6410 + 23d2f11 commit e3ab94f

File tree

10 files changed

+226
-22
lines changed

10 files changed

+226
-22
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
lgtm,codescanning
2+
* Calls to property accessors are now analyzed on par with regular function calls,
3+
leading to more results from queries that rely on data flow.

javascript/ql/src/meta/alerts/CallGraph.ql

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
import javascript
1212

13-
from DataFlow::InvokeNode invoke, Function f
14-
where invoke.getACallee() = f
15-
select invoke, "Call to $@", f, f.describe()
13+
from DataFlow::Node invoke, Function f, string kind
14+
where
15+
invoke.(DataFlow::InvokeNode).getACallee() = f and kind = "Call"
16+
or
17+
invoke.(DataFlow::PropRef).getAnAccessorCallee().getFunction() = f and kind = "Accessor call"
18+
select invoke, kind + " to $@", f, f.describe()

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

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,19 +1461,20 @@ private predicate summarizedHigherOrderCall(
14611461
DataFlow::Node arg, DataFlow::Node cb, int i, DataFlow::Configuration cfg, PathSummary summary
14621462
) {
14631463
exists(
1464-
Function f, DataFlow::InvokeNode outer, DataFlow::InvokeNode inner, int j,
1465-
DataFlow::Node innerArg, DataFlow::SourceNode cbParm, PathSummary oldSummary
1464+
Function f, DataFlow::InvokeNode inner, int j, DataFlow::Node innerArg,
1465+
DataFlow::SourceNode cbParm, PathSummary oldSummary
14661466
|
14671467
// Captured flow does not need to be summarized - it is handled by the local case in `higherOrderCall`.
1468-
not arg = DataFlow::capturedVariableNode(_) and
1469-
summarizedHigherOrderCallAux(f, outer, arg, innerArg, cfg, oldSummary, cbParm, inner, j, cb)
1468+
not arg = DataFlow::capturedVariableNode(_)
14701469
|
14711470
// direct higher-order call
1472-
cbParm.flowsTo(inner.getCalleeNode()) and
1471+
summarizedHigherOrderCallAux(f, arg, innerArg, cfg, oldSummary, cbParm, inner, j, cb) and
1472+
inner = cbParm.getAnInvocation() and
14731473
i = j and
14741474
summary = oldSummary
14751475
or
14761476
// indirect higher-order call
1477+
summarizedHigherOrderCallAux(f, arg, innerArg, cfg, oldSummary, cbParm, inner, j, cb) and
14771478
exists(DataFlow::Node cbArg, PathSummary newSummary |
14781479
cbParm.flowsTo(cbArg) and
14791480
summarizedHigherOrderCall(innerArg, cbArg, i, cfg, newSummary) and
@@ -1487,14 +1488,17 @@ private predicate summarizedHigherOrderCall(
14871488
*/
14881489
pragma[noinline]
14891490
private predicate summarizedHigherOrderCallAux(
1490-
Function f, DataFlow::InvokeNode outer, DataFlow::Node arg, DataFlow::Node innerArg,
1491-
DataFlow::Configuration cfg, PathSummary oldSummary, DataFlow::SourceNode cbParm,
1492-
DataFlow::InvokeNode inner, int j, DataFlow::Node cb
1491+
Function f, DataFlow::Node arg, DataFlow::Node innerArg, DataFlow::Configuration cfg,
1492+
PathSummary oldSummary, DataFlow::SourceNode cbParm, DataFlow::InvokeNode inner, int j,
1493+
DataFlow::Node cb
14931494
) {
1494-
reachableFromInput(f, outer, arg, innerArg, cfg, oldSummary) and
1495-
// Only track actual parameter flow.
1496-
argumentPassing(outer, cb, f, cbParm) and
1497-
innerArg = inner.getArgument(j)
1495+
exists(DataFlow::Node outer1, DataFlow::Node outer2 |
1496+
reachableFromInput(f, outer1, arg, innerArg, cfg, oldSummary) and
1497+
outer1 = pragma[only_bind_into](outer2) and
1498+
// Only track actual parameter flow.
1499+
argumentPassing(outer2, cb, f, cbParm) and
1500+
innerArg = inner.getArgument(j)
1501+
)
14981502
}
14991503

15001504
/**

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,13 @@ module DataFlow {
531531
predicate isPrivateField() {
532532
getPropertyName().charAt(0) = "#" and getPropertyNameExpr() instanceof Label
533533
}
534+
535+
/**
536+
* Gets an accessor (`get` or `set` method) that may be invoked by this property reference.
537+
*/
538+
final DataFlow::FunctionNode getAnAccessorCallee() {
539+
result = CallGraph::getAnAccessorCallee(this)
540+
}
534541
}
535542

536543
/**

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,16 @@ class ObjectLiteralNode extends DataFlow::ValueNode, DataFlow::SourceNode {
546546
DataFlow::Node getASpreadProperty() {
547547
result = astNode.getAProperty().(SpreadProperty).getInit().(SpreadElement).getOperand().flow()
548548
}
549+
550+
/** Gets the property getter of the given name, installed on this object literal. */
551+
DataFlow::FunctionNode getPropertyGetter(string name) {
552+
result = astNode.getPropertyByName(name).(PropertyGetter).getInit().flow()
553+
}
554+
555+
/** Gets the property setter of the given name, installed on this object literal. */
556+
DataFlow::FunctionNode getPropertySetter(string name) {
557+
result = astNode.getPropertyByName(name).(PropertySetter).getInit().flow()
558+
}
549559
}
550560

551561
/**

javascript/ql/src/semmle/javascript/dataflow/internal/CallGraphs.qll

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,46 @@ module CallGraph {
162162
)
163163
)
164164
}
165+
166+
/** Holds if a property setter named `name` exists in a class. */
167+
private predicate isSetterName(string name) {
168+
exists(any(DataFlow::ClassNode cls).getInstanceMember(name, DataFlow::MemberKind::setter()))
169+
}
170+
171+
/**
172+
* Gets a property write that assigns to the property `name` on an instance of this class,
173+
* and `name` is the name of a property setter.
174+
*/
175+
private DataFlow::PropWrite getAnInstanceMemberAssignment(DataFlow::ClassNode cls, string name) {
176+
isSetterName(name) and // restrict size of predicate
177+
result = cls.getAnInstanceReference().getAPropertyWrite(name)
178+
or
179+
exists(DataFlow::ClassNode subclass |
180+
result = getAnInstanceMemberAssignment(subclass, name) and
181+
not exists(subclass.getInstanceMember(name, DataFlow::MemberKind::setter())) and
182+
cls = subclass.getADirectSuperClass()
183+
)
184+
}
185+
186+
/**
187+
* Gets a getter or setter invoked as a result of the given property access.
188+
*/
189+
cached
190+
DataFlow::FunctionNode getAnAccessorCallee(DataFlow::PropRef ref) {
191+
exists(DataFlow::ClassNode cls, string name |
192+
ref = cls.getAnInstanceMemberAccess(name) and
193+
result = cls.getInstanceMember(name, DataFlow::MemberKind::getter())
194+
or
195+
ref = getAnInstanceMemberAssignment(cls, name) and
196+
result = cls.getInstanceMember(name, DataFlow::MemberKind::setter())
197+
)
198+
or
199+
exists(DataFlow::ObjectLiteralNode object, string name |
200+
ref = object.getAPropertyRead(name) and
201+
result = object.getPropertyGetter(name)
202+
or
203+
ref = object.getAPropertyWrite(name) and
204+
result = object.getPropertySetter(name)
205+
)
206+
}
165207
}

javascript/ql/src/semmle/javascript/dataflow/internal/FlowSteps.qll

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ predicate shouldTrackProperties(AbstractValue obj) {
2525
*/
2626
pragma[noinline]
2727
predicate returnExpr(Function f, DataFlow::Node source, DataFlow::Node sink) {
28-
sink.asExpr() = f.getAReturnedExpr() and source = sink
28+
sink.asExpr() = f.getAReturnedExpr() and
29+
source = sink and
30+
not f = any(SetterMethodDeclaration decl).getBody()
2931
}
3032

3133
/**
@@ -120,7 +122,11 @@ private module CachedSteps {
120122
* Holds if `invk` may invoke `f`.
121123
*/
122124
cached
123-
predicate calls(DataFlow::InvokeNode invk, Function f) { f = invk.getACallee(0) }
125+
predicate calls(DataFlow::SourceNode invk, Function f) {
126+
f = invk.(DataFlow::InvokeNode).getACallee(0)
127+
or
128+
f = invk.(DataFlow::PropRef).getAnAccessorCallee().getFunction()
129+
}
124130

125131
private predicate callsBoundInternal(
126132
DataFlow::InvokeNode invk, Function f, int boundArgs, boolean contextDependent
@@ -177,11 +183,11 @@ private module CachedSteps {
177183
*/
178184
cached
179185
predicate argumentPassing(
180-
DataFlow::InvokeNode invk, DataFlow::ValueNode arg, Function f, DataFlow::SourceNode parm
186+
DataFlow::SourceNode invk, DataFlow::Node arg, Function f, DataFlow::SourceNode parm
181187
) {
182188
calls(invk, f) and
183189
(
184-
exists(int i | arg = invk.getArgument(i) |
190+
exists(int i | arg = invk.(DataFlow::InvokeNode).getArgument(i) |
185191
exists(Parameter p |
186192
f.getParameter(i) = p and
187193
not p.isRestParameter() and
@@ -193,12 +199,19 @@ private module CachedSteps {
193199
or
194200
arg = invk.(DataFlow::CallNode).getReceiver() and
195201
parm = DataFlow::thisNode(f)
202+
or
203+
arg = invk.(DataFlow::PropRef).getBase() and
204+
parm = DataFlow::thisNode(f)
205+
or
206+
arg = invk.(DataFlow::PropWrite).getRhs() and
207+
parm = DataFlow::parameterNode(f.getParameter(0))
196208
)
197209
or
198-
exists(DataFlow::Node callback, int i, Parameter p |
210+
exists(DataFlow::Node callback, int i, Parameter p, Function target |
199211
invk.(DataFlow::PartialInvokeNode).isPartialArgument(callback, arg, i) and
200212
partiallyCalls(invk, callback, f) and
201-
f.getParameter(i) = p and
213+
f = pragma[only_bind_into](target) and
214+
target.getParameter(i) = p and
202215
not p.isRestParameter() and
203216
parm = DataFlow::parameterNode(p)
204217
)
@@ -213,7 +226,7 @@ private module CachedSteps {
213226
callsBound(invk, f, boundArgs) and
214227
f.getParameter(boundArgs + i) = p and
215228
not p.isRestParameter() and
216-
arg = invk.getArgument(i) and
229+
arg = invk.(DataFlow::InvokeNode).getArgument(i) and
217230
parm = DataFlow::parameterNode(p)
218231
)
219232
}

javascript/ql/test/library-tests/TaintTracking/BasicTaintTracking.expected

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ typeInferenceMismatch
6666
| exceptions.js:144:9:144:16 | source() | exceptions.js:132:8:132:27 | returnThrownSource() |
6767
| exceptions.js:150:13:150:20 | source() | exceptions.js:153:10:153:10 | e |
6868
| exceptions.js:158:13:158:20 | source() | exceptions.js:161:10:161:10 | e |
69+
| getters-and-setters.js:6:20:6:27 | source() | getters-and-setters.js:9:10:9:18 | new C().x |
70+
| getters-and-setters.js:6:20:6:27 | source() | getters-and-setters.js:13:18:13:20 | c.x |
71+
| getters-and-setters.js:27:15:27:22 | source() | getters-and-setters.js:23:18:23:18 | v |
72+
| getters-and-setters.js:47:23:47:30 | source() | getters-and-setters.js:45:14:45:16 | c.x |
73+
| getters-and-setters.js:60:20:60:27 | source() | getters-and-setters.js:66:10:66:14 | obj.x |
74+
| getters-and-setters.js:67:13:67:20 | source() | getters-and-setters.js:63:18:63:22 | value |
75+
| getters-and-setters.js:79:20:79:27 | source() | getters-and-setters.js:88:10:88:18 | new C().x |
76+
| getters-and-setters.js:79:20:79:27 | source() | getters-and-setters.js:92:14:92:16 | c.x |
77+
| getters-and-setters.js:79:20:79:27 | source() | getters-and-setters.js:100:10:100:22 | getX(new C()) |
78+
| getters-and-setters.js:89:17:89:24 | source() | getters-and-setters.js:82:18:82:22 | value |
6979
| importedReactComponent.jsx:4:40:4:47 | source() | exportedReactComponent.jsx:2:10:2:19 | props.text |
7080
| indexOf.js:4:11:4:18 | source() | indexOf.js:9:10:9:10 | x |
7181
| json-stringify.js:2:16:2:23 | source() | json-stringify.js:5:8:5:29 | JSON.st ... source) |

javascript/ql/test/library-tests/TaintTracking/DataFlowTracking.expected

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@
4141
| exceptions.js:144:9:144:16 | source() | exceptions.js:132:8:132:27 | returnThrownSource() |
4242
| exceptions.js:150:13:150:20 | source() | exceptions.js:153:10:153:10 | e |
4343
| exceptions.js:158:13:158:20 | source() | exceptions.js:161:10:161:10 | e |
44+
| getters-and-setters.js:6:20:6:27 | source() | getters-and-setters.js:9:10:9:18 | new C().x |
45+
| getters-and-setters.js:6:20:6:27 | source() | getters-and-setters.js:13:18:13:20 | c.x |
46+
| getters-and-setters.js:27:15:27:22 | source() | getters-and-setters.js:23:18:23:18 | v |
47+
| getters-and-setters.js:47:23:47:30 | source() | getters-and-setters.js:45:14:45:16 | c.x |
48+
| getters-and-setters.js:60:20:60:27 | source() | getters-and-setters.js:66:10:66:14 | obj.x |
49+
| getters-and-setters.js:67:13:67:20 | source() | getters-and-setters.js:63:18:63:22 | value |
50+
| getters-and-setters.js:79:20:79:27 | source() | getters-and-setters.js:88:10:88:18 | new C().x |
51+
| getters-and-setters.js:79:20:79:27 | source() | getters-and-setters.js:92:14:92:16 | c.x |
52+
| getters-and-setters.js:79:20:79:27 | source() | getters-and-setters.js:100:10:100:22 | getX(new C()) |
53+
| getters-and-setters.js:89:17:89:24 | source() | getters-and-setters.js:82:18:82:22 | value |
4454
| indexOf.js:4:11:4:18 | source() | indexOf.js:9:10:9:10 | x |
4555
| indexOf.js:4:11:4:18 | source() | indexOf.js:13:10:13:10 | x |
4656
| nested-props.js:4:13:4:20 | source() | nested-props.js:5:10:5:14 | obj.x |
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import * as dummy from 'dummy';
2+
3+
function testGetterSource() {
4+
class C {
5+
get x() {
6+
return source();
7+
}
8+
};
9+
sink(new C().x); // NOT OK
10+
11+
function indirection(c) {
12+
if (c) {
13+
sink(c.x); // NOT OK
14+
}
15+
}
16+
indirection(new C());
17+
indirection(null);
18+
}
19+
20+
function testSetterSink() {
21+
class C {
22+
set x(v) {
23+
sink(v); // NOT OK
24+
}
25+
};
26+
function indirection(c) {
27+
c.x = source();
28+
}
29+
indirection(new C());
30+
indirection(null);
31+
}
32+
33+
function testFlowThroughGetter() {
34+
class C {
35+
constructor(x) {
36+
this._x = x;
37+
}
38+
39+
get x() {
40+
return this._x;
41+
}
42+
};
43+
44+
function indirection(c) {
45+
sink(c.x); // NOT OK
46+
}
47+
indirection(new C(source()));
48+
indirection(null);
49+
50+
function getX(c) {
51+
return c.x;
52+
}
53+
sink(getX(new C(source()))); // NOT OK - but not flagged
54+
getX(null);
55+
}
56+
57+
function testFlowThroughObjectLiteralAccessors() {
58+
let obj = {
59+
get x() {
60+
return source();
61+
},
62+
set y(value) {
63+
sink(value); // NOT OK
64+
}
65+
};
66+
sink(obj.x); // NOT OK
67+
obj.y = source();
68+
69+
function indirection(c) {
70+
sink(c.x); // NOT OK - but not currently flagged
71+
}
72+
indirection(obj);
73+
indirection(null);
74+
}
75+
76+
function testFlowThroughSubclass() {
77+
class Base {
78+
get x() {
79+
return source();
80+
}
81+
set y(value) {
82+
sink(value); // NOT OK
83+
}
84+
};
85+
class C extends Base {
86+
}
87+
88+
sink(new C().x); // NOT OK
89+
new C().y = source();
90+
91+
function indirection(c) {
92+
sink(c.x); // NOT OK
93+
}
94+
indirection(new C());
95+
indirection(null);
96+
97+
function getX(c) {
98+
return c.x;
99+
}
100+
sink(getX(new C())); // NOT OK - but not flagged
101+
getX(null);
102+
}

0 commit comments

Comments
 (0)