Skip to content

Commit 4601eb9

Browse files
authored
Merge pull request #4706 from max-schaefer/issue-247
Approved by asgerf
2 parents 36ad6b3 + f40b406 commit 4601eb9

File tree

10 files changed

+164
-36
lines changed

10 files changed

+164
-36
lines changed

javascript/ql/src/semmle/javascript/ApiGraphs.qll

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ module API {
206206
this = Impl::MkClassInstance(result) or
207207
this = Impl::MkUse(result) or
208208
this = Impl::MkDef(result) or
209-
this = Impl::MkAsyncFuncResult(result)
209+
this = Impl::MkAsyncFuncResult(result) or
210+
this = Impl::MkSyntheticCallbackArg(_, _, result)
210211
}
211212

212213
/**
@@ -389,12 +390,16 @@ module API {
389390
MkCanonicalNameUse(CanonicalName n) {
390391
not n.isRoot() and
391392
isUsed(n)
393+
} or
394+
MkSyntheticCallbackArg(DataFlow::Node src, int bound, DataFlow::InvokeNode nd) {
395+
trackUseNode(src, true, bound).flowsTo(nd.getCalleeNode())
392396
}
393397

394398
class TDef = MkModuleDef or TNonModuleDef;
395399

396400
class TNonModuleDef =
397-
MkModuleExport or MkClassInstance or MkAsyncFuncResult or MkDef or MkCanonicalNameDef;
401+
MkModuleExport or MkClassInstance or MkAsyncFuncResult or MkDef or MkCanonicalNameDef or
402+
MkSyntheticCallbackArg;
398403

399404
class TUse = MkModuleUse or MkModuleImport or MkUse or MkCanonicalNameUse;
400405

@@ -523,18 +528,20 @@ module API {
523528
* The receiver is considered to be argument -1.
524529
*/
525530
private predicate argumentPassing(TApiNode base, int i, DataFlow::Node arg) {
526-
exists(DataFlow::SourceNode use, DataFlow::SourceNode pred |
527-
use(base, use) and pred = trackUseNode(use)
531+
exists(DataFlow::Node use, DataFlow::SourceNode pred, int bound |
532+
use(base, use) and pred = trackUseNode(use, _, bound)
528533
|
529-
arg = pred.getAnInvocation().getArgument(i)
534+
arg = pred.getAnInvocation().getArgument(i - bound)
530535
or
531536
arg = pred.getACall().getReceiver() and
537+
bound = 0 and
532538
i = -1
533539
or
534540
exists(DataFlow::PartialInvokeNode pin, DataFlow::Node callback | pred.flowsTo(callback) |
535-
pin.isPartialArgument(callback, arg, i)
541+
pin.isPartialArgument(callback, arg, i - bound)
536542
or
537543
arg = pin.getBoundReceiver(callback) and
544+
bound = 0 and
538545
i = -1
539546
)
540547
)
@@ -609,6 +616,12 @@ module API {
609616
lbl = Label::instance() and
610617
ref = getANodeWithType(tn)
611618
)
619+
or
620+
exists(DataFlow::InvokeNode call |
621+
base = MkSyntheticCallbackArg(_, _, call) and
622+
lbl = Label::parameter(1) and
623+
ref = awaited(call)
624+
)
612625
)
613626
}
614627

@@ -672,20 +685,70 @@ module API {
672685
)
673686
}
674687

675-
private DataFlow::SourceNode trackUseNode(DataFlow::SourceNode nd, DataFlow::TypeTracker t) {
688+
/**
689+
* Gets a data-flow node to which `nd`, which is a use of an API-graph node, flows.
690+
*
691+
* The flow from `nd` to that node may be inter-procedural. If `promisified` is `true`, the
692+
* flow goes through a promisification, and `boundArgs` indicates how many arguments have been
693+
* bound throughout the flow. (To ensure termination, we somewhat arbitrarily constrain the
694+
* number of bound arguments to be at most ten.)
695+
*/
696+
private DataFlow::SourceNode trackUseNode(
697+
DataFlow::SourceNode nd, boolean promisified, int boundArgs, DataFlow::TypeTracker t
698+
) {
676699
t.start() and
677700
use(_, nd) and
678-
result = nd
701+
result = nd and
702+
promisified = false and
703+
boundArgs = 0
704+
or
705+
exists(DataFlow::CallNode promisify |
706+
promisify = DataFlow::moduleImport(["util", "bluebird"]).getAMemberCall("promisify")
707+
|
708+
trackUseNode(nd, false, boundArgs, t.continue()).flowsTo(promisify.getArgument(0)) and
709+
promisified = true and
710+
result = promisify
711+
)
712+
or
713+
exists(DataFlow::PartialInvokeNode pin, DataFlow::Node pred, int predBoundArgs |
714+
trackUseNode(nd, promisified, predBoundArgs, t.continue()).flowsTo(pred) and
715+
result = pin.getBoundFunction(pred, boundArgs - predBoundArgs) and
716+
boundArgs in [0 .. 10]
717+
)
679718
or
680-
exists(DataFlow::TypeTracker t2 | result = trackUseNode(nd, t2).track(t2, t))
719+
exists(StepSummary summary |
720+
t = useStep(nd, promisified, boundArgs, result, summary).append(summary)
721+
)
722+
}
723+
724+
private import semmle.javascript.dataflow.internal.StepSummary
725+
726+
/**
727+
* Holds if `nd`, which is a use of an API-graph node, flows in zero or more potentially
728+
* inter-procedural steps to some intermediate node, and then from that intermediate node to
729+
* `res` in one step described by `summary`.
730+
*
731+
* This predicate exists solely to enforce a better join order in `trackUseNode` above.
732+
*/
733+
pragma[noinline]
734+
private DataFlow::TypeTracker useStep(
735+
DataFlow::Node nd, boolean promisified, int boundArgs, DataFlow::Node res, StepSummary summary
736+
) {
737+
StepSummary::step(trackUseNode(nd, promisified, boundArgs, result), res, summary)
738+
}
739+
740+
private DataFlow::SourceNode trackUseNode(
741+
DataFlow::SourceNode nd, boolean promisified, int boundArgs
742+
) {
743+
result = trackUseNode(nd, promisified, boundArgs, DataFlow::TypeTracker::end())
681744
}
682745

683746
/**
684747
* Gets a node that is inter-procedurally reachable from `nd`, which is a use of some node.
685748
*/
686749
cached
687750
DataFlow::SourceNode trackUseNode(DataFlow::SourceNode nd) {
688-
result = trackUseNode(nd, DataFlow::TypeTracker::end())
751+
result = trackUseNode(nd, false, 0)
689752
}
690753

691754
private DataFlow::SourceNode trackDefNode(DataFlow::Node nd, DataFlow::TypeBackTracker t) {
@@ -714,6 +777,21 @@ module API {
714777
result = trackDefNode(nd, DataFlow::TypeBackTracker::end())
715778
}
716779

780+
private DataFlow::SourceNode awaited(DataFlow::InvokeNode call, DataFlow::TypeTracker t) {
781+
t.startInPromise() and
782+
exists(MkSyntheticCallbackArg(_, _, call)) and
783+
result = call
784+
or
785+
exists(DataFlow::TypeTracker t2 | result = awaited(call, t2).track(t2, t))
786+
}
787+
788+
/**
789+
* Gets a node holding the resolved value of promise `call`.
790+
*/
791+
private DataFlow::Node awaited(DataFlow::InvokeNode call) {
792+
result = awaited(call, DataFlow::TypeTracker::end())
793+
}
794+
717795
private DataFlow::SourceNode getANodeWithType(TypeName tn) {
718796
exists(string moduleName, string typeName |
719797
tn.hasQualifiedName(moduleName, typeName) and
@@ -774,6 +852,12 @@ module API {
774852
lbl = Label::return() and
775853
succ = MkAsyncFuncResult(f)
776854
)
855+
or
856+
exists(DataFlow::SourceNode src, int bound, DataFlow::InvokeNode call |
857+
use(pred, src) and
858+
lbl = Label::parameter(bound + call.getNumArgument()) and
859+
succ = MkSyntheticCallbackArg(src, bound, call)
860+
)
777861
}
778862

779863
/**

javascript/ql/src/semmle/javascript/frameworks/NoSQL.qll

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,6 @@ private module Redis {
804804
* For getter-like methods it is not generally possible to gain access "outside" of where you are supposed to have access,
805805
* it is at most possible to get a Redis call to return more results than expected (e.g. by adding more members to [`geohash`](https://redis.io/commands/geohash)).
806806
*/
807-
bindingset[argIndex]
808807
predicate argumentIsAmbiguousKey(string method, int argIndex) {
809808
method =
810809
[
@@ -815,7 +814,8 @@ private module Redis {
815814
] and
816815
argIndex = 0
817816
or
818-
method = ["bitop", "hmset", "mset", "msetnx", "geoadd"] and argIndex >= 0
817+
method = ["bitop", "hmset", "mset", "msetnx", "geoadd"] and
818+
argIndex in [0 .. any(DataFlow::InvokeNode invk).getNumArgument() - 1]
819819
}
820820
}
821821

@@ -825,31 +825,9 @@ private module Redis {
825825
class RedisKeyArgument extends NoSQL::Query {
826826
RedisKeyArgument() {
827827
exists(string method, int argIndex |
828-
QuerySignatures::argumentIsAmbiguousKey(method, argIndex)
829-
|
830-
this =
831-
[promisify(redis().getMember(method)), redis().getMember(method)]
832-
.getACall()
833-
.getArgument(argIndex)
834-
.asExpr()
828+
QuerySignatures::argumentIsAmbiguousKey(method, argIndex) and
829+
this = redis().getMember(method).getParameter(argIndex).getARhs().asExpr()
835830
)
836831
}
837832
}
838-
839-
/**
840-
* Gets a promisified version of `method`.
841-
*/
842-
private API::Node promisify(API::Node method) {
843-
exists(API::Node promisify |
844-
promisify = API::moduleImport(["util", "bluebird"]).getMember("promisify").getReturn() and
845-
method
846-
.getAnImmediateUse()
847-
.flowsTo(promisify.getAnImmediateUse().(DataFlow::CallNode).getArgument(0))
848-
|
849-
result = promisify
850-
or
851-
result = promisify.getMember("bind").getReturn() and
852-
result.getAnImmediateUse().(DataFlow::CallNode).getNumArgument() = 1
853-
)
854-
}
855833
}

javascript/ql/test/ApiGraphs/bound-args/VerifyAssertions.expected

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import ApiGraphs.VerifyAssertions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import bar from 'foo';
2+
3+
let boundbar = bar.bind(
4+
"receiver", // def (parameter -1 (member default (member exports (module foo))))
5+
"firstarg" // def (parameter 0 (member default (member exports (module foo))))
6+
);
7+
boundbar(
8+
"secondarg" // def (parameter 1 (member default (member exports (module foo))))
9+
)
10+
11+
let boundbar2 = boundbar.bind(
12+
"ignored", // !def (parameter -1 (member default (member exports (module foo))))
13+
"othersecondarg" // def (parameter 1 (member default (member exports (module foo))))
14+
)
15+
boundbar2(
16+
"thirdarg" // def (parameter 2 (member default (member exports (module foo))))
17+
)
18+
19+
let bar2 = bar;
20+
for (var i = 0; i < 2; ++i)
21+
bar2 = bar2.bind(
22+
null,
23+
i /* def (parameter 1 (member default (member exports (module foo)))) */ /* def (parameter 9 (member default (member exports (module foo)))) */
24+
);
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "bound-args",
3+
"dependencies": {
4+
"foo": "*"
5+
}
6+
}

javascript/ql/test/ApiGraphs/promisify/VerifyAssertions.expected

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import ApiGraphs.VerifyAssertions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
var bluebird = require("bluebird");
2+
var readFile = require("fs").readFile;
3+
4+
var readFileAsync = bluebird.promisify(readFile);
5+
6+
readFile(
7+
"tst.txt", // def (parameter 0 (member readFile (member exports (module fs))))
8+
"utf8", // def (parameter 1 (member readFile (member exports (module fs))))
9+
function (
10+
err, // use (parameter 0 (parameter 2 (member readFile (member exports (module fs)))))
11+
contents // use (parameter 1 (parameter 2 (member readFile (member exports (module fs)))))
12+
) { });
13+
14+
readFileAsync(
15+
"tst.txt" // def (parameter 0 (member readFile (member exports (module fs))))
16+
).then(
17+
function (buf) { } // use (parameter 1 (parameter 1 (member readFile (member exports (module fs)))))
18+
).catch(
19+
function (err) { } // not yet modelled: (parameter 0 (parameter 1 (member readFile (member exports (module fs)))))
20+
);
21+
22+
try {
23+
let p = readFileAsync(
24+
"tst.txt", // def (parameter 0 (member readFile (member exports (module fs))))
25+
"utf8" // def (parameter 1 (member readFile (member exports (module fs))))
26+
);
27+
let data = await p; // use (parameter 1 (parameter 2 (member readFile (member exports (module fs)))))
28+
} catch (e) { } // not yet modelled: (parameter 0 (parameter 2 (member readFile (member exports (module fs)))))
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "promisify-test",
3+
"dependencies": {
4+
"bluebird": "*"
5+
}
6+
}

0 commit comments

Comments
 (0)