Skip to content

Commit b0b152a

Browse files
authored
Merge pull request #1529 from xiemaisi/js/getter-summaries
Approved by asger-semmle
2 parents 3b126d9 + 7f95c20 commit b0b152a

File tree

10 files changed

+153
-19
lines changed

10 files changed

+153
-19
lines changed

change-notes/1.22/analysis-javascript.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
- [exec-async](https://www.npmjs.com/package/exec-async)
1111
- [express](https://www.npmjs.com/package/express)
1212
- [remote-exec](https://www.npmjs.com/package/remote-exec)
13+
14+
* Support for tracking data flow and taint through getter functions (that is, functions that return a property of one of their arguments) and through the receiver object of method calls has been improved. This may produce more security alerts.
1315

1416
## New queries
1517

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

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ private predicate exploratoryFlowStep(
521521
) {
522522
basicFlowStep(pred, succ, _, cfg) or
523523
basicStoreStep(pred, succ, _) or
524-
loadStep(pred, succ, _) or
524+
basicLoadStep(pred, succ, _) or
525525
// the following two disjuncts taken together over-approximate flow through
526526
// higher-order calls
527527
callback(pred, succ) or
@@ -697,11 +697,60 @@ private predicate storeStep(
697697
)
698698
}
699699

700+
/**
701+
* Holds if `f` may `read` property `prop` of parameter `parm`.
702+
*/
703+
private predicate parameterPropRead(
704+
Function f, DataFlow::Node invk, DataFlow::Node arg, string prop, DataFlow::PropRead read,
705+
DataFlow::Configuration cfg
706+
) {
707+
exists(DataFlow::SourceNode parm |
708+
callInputStep(f, invk, arg, parm, cfg) and
709+
read = parm.getAPropertyRead(prop)
710+
)
711+
}
712+
713+
/**
714+
* Holds if `nd` may flow into a return statement of `f` under configuration `cfg`
715+
* (possibly through callees) along a path summarized by `summary`.
716+
*/
717+
private predicate reachesReturn(
718+
Function f, DataFlow::Node nd, DataFlow::Configuration cfg, PathSummary summary
719+
) {
720+
isRelevant(nd, cfg) and
721+
returnExpr(f, nd, _) and
722+
summary = PathSummary::level()
723+
or
724+
exists(DataFlow::Node mid, PathSummary oldSummary, PathSummary newSummary |
725+
flowStep(nd, cfg, mid, oldSummary) and
726+
reachesReturn(f, mid, cfg, newSummary) and
727+
summary = oldSummary.append(newSummary)
728+
)
729+
}
730+
731+
/**
732+
* Holds if property `prop` of `pred` may flow into `succ` along a path summarized by
733+
* `summary`.
734+
*/
735+
private predicate loadStep(
736+
DataFlow::Node pred, DataFlow::Node succ, string prop, DataFlow::Configuration cfg,
737+
PathSummary summary
738+
) {
739+
basicLoadStep(pred, succ, prop) and
740+
summary = PathSummary::level()
741+
or
742+
exists(Function f, DataFlow::PropRead read |
743+
parameterPropRead(f, succ, pred, prop, read, cfg) and
744+
reachesReturn(f, read, cfg, summary)
745+
)
746+
}
747+
700748
/**
701749
* Holds if `rhs` is the right-hand side of a write to property `prop`, and `nd` is reachable
702750
* from the base of that write under configuration `cfg` (possibly through callees) along a
703751
* path summarized by `summary`.
704752
*/
753+
pragma[nomagic]
705754
private predicate reachableFromStoreBase(
706755
string prop, DataFlow::Node rhs, DataFlow::Node nd, DataFlow::Configuration cfg,
707756
PathSummary summary
@@ -723,11 +772,14 @@ private predicate reachableFromStoreBase(
723772
*
724773
* In other words, `pred` may flow to `succ` through a property.
725774
*/
775+
pragma[noinline]
726776
private predicate flowThroughProperty(
727777
DataFlow::Node pred, DataFlow::Node succ, DataFlow::Configuration cfg, PathSummary summary
728778
) {
729-
exists(string prop, DataFlow::Node base | reachableFromStoreBase(prop, pred, base, cfg, summary) |
730-
loadStep(base, succ, prop)
779+
exists(string prop, DataFlow::Node base, PathSummary oldSummary, PathSummary newSummary |
780+
reachableFromStoreBase(prop, pred, base, cfg, oldSummary) and
781+
loadStep(base, succ, prop, cfg, newSummary) and
782+
summary = oldSummary.append(newSummary)
731783
)
732784
}
733785

@@ -743,7 +795,7 @@ private predicate summarizedHigherOrderCall(
743795
) {
744796
exists(
745797
Function f, DataFlow::InvokeNode outer, DataFlow::InvokeNode inner, int j,
746-
DataFlow::Node innerArg, DataFlow::ParameterNode cbParm, PathSummary oldSummary
798+
DataFlow::Node innerArg, DataFlow::SourceNode cbParm, PathSummary oldSummary
747799
|
748800
reachableFromInput(f, outer, arg, innerArg, cfg, oldSummary) and
749801
argumentPassing(outer, cb, f, cbParm) and
@@ -786,6 +838,7 @@ private predicate summarizedHigherOrderCall(
786838
* - The flow label mapping of the summary corresponds to the transformation from `arg` to the
787839
* invocation of the callback.
788840
*/
841+
pragma[nomagic]
789842
private predicate higherOrderCall(
790843
DataFlow::Node arg, DataFlow::SourceNode callback, int i, DataFlow::Configuration cfg,
791844
PathSummary summary

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

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ private module NodeTracking {
102102
or
103103
basicStoreStep(mid, nd, _)
104104
or
105-
loadStep(mid, nd, _)
105+
basicLoadStep(mid, nd, _)
106106
or
107107
callback(mid, nd)
108108
or
@@ -150,6 +150,21 @@ private module NodeTracking {
150150
)
151151
}
152152

153+
/**
154+
* Holds if `nd` may flow into a return statement of `f`
155+
* (possibly through callees) along a path summarized by `summary`.
156+
*/
157+
private predicate reachesReturn(Function f, DataFlow::Node nd, PathSummary summary) {
158+
returnExpr(f, nd, _) and
159+
summary = PathSummary::level()
160+
or
161+
exists (DataFlow::Node mid, PathSummary oldSummary, PathSummary newSummary |
162+
flowStep(nd, mid, oldSummary) and
163+
reachesReturn(f, mid, newSummary) and
164+
summary = oldSummary.append(newSummary)
165+
)
166+
}
167+
153168
/**
154169
* Holds if a function invoked at `invk` may return an expression into which `input`,
155170
* which is either an argument or a definition captured by the function, flows,
@@ -191,6 +206,21 @@ private module NodeTracking {
191206
)
192207
}
193208

209+
/**
210+
* Holds if property `prop` of `pred` may flow into `succ` along a path summarized by
211+
* `summary`.
212+
*/
213+
private predicate loadStep(DataFlow::Node pred, DataFlow::Node succ, string prop,
214+
PathSummary summary) {
215+
basicLoadStep(pred, succ, prop) and
216+
summary = PathSummary::level()
217+
or
218+
exists (Function f, DataFlow::SourceNode parm |
219+
argumentPassing(succ, pred, f, parm) and
220+
reachesReturn(f, parm.getAPropertyRead(prop), summary)
221+
)
222+
}
223+
194224
/**
195225
* Holds if `rhs` is the right-hand side of a write to property `prop`, and `nd` is reachable
196226
* from the base of that write (possibly through callees) along a path summarized by `summary`.
@@ -216,9 +246,10 @@ private module NodeTracking {
216246
private predicate flowThroughProperty(
217247
DataFlow::Node pred, DataFlow::Node succ, PathSummary summary
218248
) {
219-
exists(string prop, DataFlow::Node base |
220-
reachableFromStoreBase(prop, pred, base, summary) and
221-
loadStep(base, succ, prop)
249+
exists (string prop, DataFlow::Node base, PathSummary oldSummary, PathSummary newSummary |
250+
reachableFromStoreBase(prop, pred, base, oldSummary) and
251+
loadStep(base, succ, prop, newSummary) and
252+
summary = oldSummary.append(newSummary)
222253
)
223254
}
224255

@@ -232,7 +263,7 @@ private module NodeTracking {
232263
) {
233264
exists(
234265
Function f, DataFlow::InvokeNode outer, DataFlow::InvokeNode inner, int j,
235-
DataFlow::Node innerArg, DataFlow::ParameterNode cbParm, PathSummary oldSummary
266+
DataFlow::Node innerArg, DataFlow::SourceNode cbParm, PathSummary oldSummary
236267
|
237268
reachableFromInput(f, outer, arg, innerArg, oldSummary) and
238269
argumentPassing(outer, cb, f, cbParm) and

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ module StepSummary {
8383
basicStoreStep(pred, succ, prop) and
8484
summary = StoreStep(prop)
8585
or
86-
loadStep(pred, succ, prop) and
86+
basicLoadStep(pred, succ, prop) and
8787
summary = LoadStep(prop)
8888
)
8989
}

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,20 +125,27 @@ private module CachedSteps {
125125
*/
126126
cached
127127
predicate argumentPassing(
128-
DataFlow::InvokeNode invk, DataFlow::ValueNode arg, Function f, DataFlow::ParameterNode parm
128+
DataFlow::InvokeNode invk, DataFlow::ValueNode arg, Function f, DataFlow::SourceNode parm
129129
) {
130130
calls(invk, f) and
131-
exists(int i |
132-
f.getParameter(i) = parm.getParameter() and
133-
not parm.isRestParameter() and
134-
arg = invk.getArgument(i)
131+
(
132+
exists(int i, Parameter p |
133+
f.getParameter(i) = p and
134+
not p.isRestParameter() and
135+
arg = invk.getArgument(i) and
136+
parm = DataFlow::parameterNode(p)
137+
)
138+
or
139+
arg = invk.(DataFlow::CallNode).getReceiver() and
140+
parm = DataFlow::thisNode(f)
135141
)
136142
or
137-
exists(DataFlow::Node callback, int i |
143+
exists(DataFlow::Node callback, int i, Parameter p |
138144
invk.(DataFlow::AdditionalPartialInvokeNode).isPartialArgument(callback, arg, i) and
139145
partiallyCalls(invk, callback, f) and
140-
parm.getParameter() = f.getParameter(i) and
141-
not parm.isRestParameter()
146+
f.getParameter(i) = p and
147+
not p.isRestParameter() and
148+
parm = DataFlow::parameterNode(p)
142149
)
143150
}
144151

@@ -296,7 +303,7 @@ private module CachedSteps {
296303
* that is, `succ` is a read of property `prop` from `pred`.
297304
*/
298305
cached
299-
predicate loadStep(DataFlow::Node pred, DataFlow::PropRead succ, string prop) {
306+
predicate basicLoadStep(DataFlow::Node pred, DataFlow::PropRead succ, string prop) {
300307
succ.accesses(pred, prop)
301308
}
302309

javascript/ql/test/library-tests/InterProceduralFlow/DataFlow.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
| promises.js:12:22:12:31 | "rejected" | promises.js:24:20:24:20 | v |
3030
| promises.js:12:22:12:31 | "rejected" | promises.js:27:16:27:16 | v |
3131
| properties2.js:7:14:7:21 | "source" | properties2.js:8:12:8:24 | foo(source).p |
32+
| properties2.js:7:14:7:21 | "source" | properties2.js:33:13:33:20 | getP(o3) |
3233
| properties.js:2:16:2:24 | "tainted" | properties.js:5:14:5:23 | a.someProp |
3334
| properties.js:2:16:2:24 | "tainted" | properties.js:12:15:12:24 | x.someProp |
3435
| properties.js:2:16:2:24 | "tainted" | properties.js:14:15:14:27 | tmp1.someProp |
@@ -37,6 +38,7 @@
3738
| tst2.js:3:17:3:26 | "tainted2" | tst2.js:11:15:11:24 | g(source2) |
3839
| tst2.js:6:24:6:37 | "also tainted" | tst2.js:10:15:10:24 | g(source1) |
3940
| tst2.js:6:24:6:37 | "also tainted" | tst2.js:11:15:11:24 | g(source2) |
41+
| tst6.mjs:12:14:12:21 | "source" | tst6.mjs:14:12:14:16 | a.m() |
4042
| tst.js:2:17:2:22 | "src1" | tst.js:39:17:39:17 | x |
4143
| tst.js:2:17:2:22 | "src1" | tst.js:41:19:41:19 | x |
4244
| tst.js:2:17:2:22 | "src1" | tst.js:45:17:45:17 | x |

javascript/ql/test/library-tests/InterProceduralFlow/GermanFlow.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
| promises.js:12:22:12:31 | "rejected" | promises.js:24:20:24:20 | v |
3131
| promises.js:12:22:12:31 | "rejected" | promises.js:27:16:27:16 | v |
3232
| properties2.js:7:14:7:21 | "source" | properties2.js:8:12:8:24 | foo(source).p |
33+
| properties2.js:7:14:7:21 | "source" | properties2.js:33:13:33:20 | getP(o3) |
3334
| properties.js:2:16:2:24 | "tainted" | properties.js:5:14:5:23 | a.someProp |
3435
| properties.js:2:16:2:24 | "tainted" | properties.js:12:15:12:24 | x.someProp |
3536
| properties.js:2:16:2:24 | "tainted" | properties.js:14:15:14:27 | tmp1.someProp |
@@ -38,6 +39,7 @@
3839
| tst2.js:3:17:3:26 | "tainted2" | tst2.js:11:15:11:24 | g(source2) |
3940
| tst2.js:6:24:6:37 | "also tainted" | tst2.js:10:15:10:24 | g(source1) |
4041
| tst2.js:6:24:6:37 | "also tainted" | tst2.js:11:15:11:24 | g(source2) |
42+
| tst6.mjs:12:14:12:21 | "source" | tst6.mjs:14:12:14:16 | a.m() |
4143
| tst.js:2:17:2:22 | "src1" | tst.js:39:17:39:17 | x |
4244
| tst.js:2:17:2:22 | "src1" | tst.js:41:19:41:19 | x |
4345
| tst.js:2:17:2:22 | "src1" | tst.js:45:17:45:17 | x |

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
| promises.js:12:22:12:31 | "rejected" | promises.js:27:16:27:16 | v |
3737
| promises.js:32:24:32:37 | "also tainted" | promises.js:38:32:38:32 | v |
3838
| properties2.js:7:14:7:21 | "source" | properties2.js:8:12:8:24 | foo(source).p |
39+
| properties2.js:7:14:7:21 | "source" | properties2.js:33:13:33:20 | getP(o3) |
3940
| properties.js:2:16:2:24 | "tainted" | properties.js:5:14:5:23 | a.someProp |
4041
| properties.js:2:16:2:24 | "tainted" | properties.js:12:15:12:24 | x.someProp |
4142
| properties.js:2:16:2:24 | "tainted" | properties.js:14:15:14:27 | tmp1.someProp |
@@ -47,6 +48,7 @@
4748
| tst4.js:2:16:2:24 | "tainted" | tst4.js:15:15:15:31 | id(still_tainted) |
4849
| tst4.js:2:16:2:24 | "tainted" | tst4.js:16:15:16:28 | p.also_tainted |
4950
| tst4.js:2:16:2:24 | "tainted" | tst4.js:17:15:17:28 | substr(source) |
51+
| tst6.mjs:12:14:12:21 | "source" | tst6.mjs:14:12:14:16 | a.m() |
5052
| tst.js:2:17:2:22 | "src1" | tst.js:3:15:3:29 | String(source1) |
5153
| tst.js:2:17:2:22 | "src1" | tst.js:4:15:4:29 | RegExp(source1) |
5254
| tst.js:2:17:2:22 | "src1" | tst.js:5:15:5:33 | new String(source1) |

javascript/ql/test/library-tests/InterProceduralFlow/properties2.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,25 @@ var o2 = {};
2121
setP(o2, "not a source");
2222
var sink5 = o2.p;
2323

24+
function getP(base) {
25+
return base.p;
26+
}
27+
28+
function getQ(base) {
29+
return base.q;
30+
}
31+
32+
var o3 = { p: source };
33+
var sink6 = getP(o3);
34+
var sink7 = getQ(o3);
35+
36+
var o4 = {};
37+
setP(o4, source);
38+
var sink8 = getP(o4);
39+
var sink9 = getQ(o4);
40+
41+
var o5 = {};
42+
setP(o5, "not a source");
43+
var sink10 = getP(o5);
44+
2445
// semmle-extractor-options: --source-type module
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class A {
2+
constructor(f) {
3+
this._f = f;
4+
5+
}
6+
7+
m() {
8+
return this._f;
9+
}
10+
}
11+
12+
var source = "source";
13+
var a = new A(source);
14+
var sink = a.m();

0 commit comments

Comments
 (0)