Skip to content

Commit bc91f66

Browse files
author
Max Schaefer
committed
JavaScript: Teach API graphs to handle some forms of property copying.
In particular, copied promises are now handled better.
1 parent db3c99d commit bc91f66

File tree

3 files changed

+73
-23
lines changed

3 files changed

+73
-23
lines changed

javascript/ql/lib/semmle/javascript/ApiGraphs.qll

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ module API {
411411
any(Type t).hasUnderlyingType(moduleName, exportName)
412412
} or
413413
MkSyntheticCallbackArg(DataFlow::Node src, int bound, DataFlow::InvokeNode nd) {
414-
trackUseNode(src, true, bound).flowsTo(nd.getCalleeNode())
414+
trackUseNode(src, true, bound, "").flowsTo(nd.getCalleeNode())
415415
}
416416

417417
class TDef = MkModuleDef or TNonModuleDef;
@@ -528,7 +528,7 @@ module API {
528528
*/
529529
private predicate argumentPassing(TApiNode base, int i, DataFlow::Node arg) {
530530
exists(DataFlow::Node use, DataFlow::SourceNode pred, int bound |
531-
use(base, use) and pred = trackUseNode(use, _, bound)
531+
use(base, use) and pred = trackUseNode(use, _, bound, "")
532532
|
533533
arg = pred.getAnInvocation().getArgument(i - bound)
534534
or
@@ -567,26 +567,37 @@ module API {
567567
base = MkRoot() and
568568
ref = lbl.(EntryPoint).getAUse()
569569
or
570-
exists(DataFlow::SourceNode src, DataFlow::SourceNode pred |
571-
use(base, src) and pred = trackUseNode(src)
570+
exists(DataFlow::SourceNode src, DataFlow::SourceNode pred, string prop |
571+
use(base, src) and pred = trackUseNode(src, false, 0, prop)
572572
|
573573
// `module.exports` is special: it is a use of a def-node, not a use-node,
574574
// so we want to exclude it here
575575
(base instanceof TNonModuleDef or base instanceof TUse) and
576576
lbl = Label::memberFromRef(ref) and
577+
(
578+
lbl = Label::member(prop)
579+
or
580+
prop = ""
581+
) and
577582
ref = pred.getAPropertyRead()
578583
or
579584
lbl = Label::instance() and
585+
prop = "" and
580586
ref = pred.getAnInstantiation()
581587
or
582588
lbl = Label::return() and
589+
prop = "" and
583590
ref = pred.getAnInvocation()
584591
or
585-
lbl = Label::promised() and
586-
PromiseFlow::loadStep(pred.getALocalUse(), ref, Promises::valueProp())
587-
or
588-
lbl = Label::promisedError() and
589-
PromiseFlow::loadStep(pred.getALocalUse(), ref, Promises::errorProp())
592+
(
593+
lbl = Label::promised() and
594+
(prop = Promises::valueProp() or prop = "") and
595+
PromiseFlow::loadStep(pred.getALocalUse(), ref, Promises::valueProp())
596+
or
597+
lbl = Label::promisedError() and
598+
(prop = Promises::errorProp() or prop = "") and
599+
PromiseFlow::loadStep(pred.getALocalUse(), ref, Promises::errorProp())
600+
)
590601
)
591602
or
592603
exists(DataFlow::Node def, DataFlow::FunctionNode fn |
@@ -680,36 +691,58 @@ module API {
680691
)
681692
}
682693

694+
private import semmle.javascript.dataflow.TypeTracking
695+
683696
/**
684697
* Gets a data-flow node to which `nd`, which is a use of an API-graph node, flows.
685698
*
686-
* The flow from `nd` to that node may be inter-procedural. If `promisified` is `true`, the
687-
* flow goes through a promisification, and `boundArgs` indicates how many arguments have been
688-
* bound throughout the flow. (To ensure termination, we somewhat arbitrarily constrain the
689-
* number of bound arguments to be at most ten.)
699+
* The flow from `nd` to that node may be inter-procedural, and is further described by three
700+
* flags:
701+
*
702+
* - `promisified`: if true `true`, the flow goes through a promisification;
703+
* - `boundArgs`: for function values, tracks how many arguments have been bound throughout
704+
* the flow. To ensure termination, we somewhat arbitrarily constrain the number of bound
705+
* arguments to be at most ten.
706+
* - `prop`: if non-empty, the flow is only guaranteed to preserve the value of this property,
707+
* and not necessarily the entire object.
690708
*/
691709
private DataFlow::SourceNode trackUseNode(
692-
DataFlow::SourceNode nd, boolean promisified, int boundArgs, DataFlow::TypeTracker t
710+
DataFlow::SourceNode nd, boolean promisified, int boundArgs, string prop,
711+
DataFlow::TypeTracker t
693712
) {
694713
t.start() and
695714
use(_, nd) and
696715
result = nd and
697716
promisified = false and
698-
boundArgs = 0
717+
boundArgs = 0 and
718+
prop = ""
699719
or
700720
exists(Promisify::PromisifyCall promisify |
701-
trackUseNode(nd, false, boundArgs, t.continue()).flowsTo(promisify.getArgument(0)) and
721+
trackUseNode(nd, false, boundArgs, prop, t.continue()).flowsTo(promisify.getArgument(0)) and
702722
promisified = true and
723+
prop = "" and
703724
result = promisify
704725
)
705726
or
706727
exists(DataFlow::PartialInvokeNode pin, DataFlow::Node pred, int predBoundArgs |
707-
trackUseNode(nd, promisified, predBoundArgs, t.continue()).flowsTo(pred) and
728+
trackUseNode(nd, promisified, predBoundArgs, prop, t.continue()).flowsTo(pred) and
729+
prop = "" and
708730
result = pin.getBoundFunction(pred, boundArgs - predBoundArgs) and
709731
boundArgs in [0 .. 10]
710732
)
711733
or
712-
t = useStep(nd, promisified, boundArgs, result)
734+
exists(DataFlow::Node pred, string preprop |
735+
trackUseNode(nd, promisified, boundArgs, preprop, t.continue()).flowsTo(pred) and
736+
promisified = false and
737+
boundArgs = 0 and
738+
SharedTypeTrackingStep::loadStoreStep(pred, result, prop)
739+
|
740+
prop = preprop
741+
or
742+
preprop = ""
743+
)
744+
or
745+
t = useStep(nd, promisified, boundArgs, prop, result)
713746
}
714747

715748
private import semmle.javascript.dataflow.internal.StepSummary
@@ -723,27 +756,27 @@ module API {
723756
*/
724757
pragma[noopt]
725758
private DataFlow::TypeTracker useStep(
726-
DataFlow::Node nd, boolean promisified, int boundArgs, DataFlow::Node res
759+
DataFlow::Node nd, boolean promisified, int boundArgs, string prop, DataFlow::Node res
727760
) {
728761
exists(DataFlow::TypeTracker t, StepSummary summary, DataFlow::SourceNode prev |
729-
prev = trackUseNode(nd, promisified, boundArgs, t) and
762+
prev = trackUseNode(nd, promisified, boundArgs, prop, t) and
730763
StepSummary::step(prev, res, summary) and
731764
result = t.append(summary)
732765
)
733766
}
734767

735768
private DataFlow::SourceNode trackUseNode(
736-
DataFlow::SourceNode nd, boolean promisified, int boundArgs
769+
DataFlow::SourceNode nd, boolean promisified, int boundArgs, string prop
737770
) {
738-
result = trackUseNode(nd, promisified, boundArgs, DataFlow::TypeTracker::end())
771+
result = trackUseNode(nd, promisified, boundArgs, prop, DataFlow::TypeTracker::end())
739772
}
740773

741774
/**
742775
* Gets a node that is inter-procedurally reachable from `nd`, which is a use of some node.
743776
*/
744777
cached
745778
DataFlow::SourceNode trackUseNode(DataFlow::SourceNode nd) {
746-
result = trackUseNode(nd, false, 0)
779+
result = trackUseNode(nd, false, 0, "")
747780
}
748781

749782
private DataFlow::SourceNode trackDefNode(DataFlow::Node nd, DataFlow::TypeBackTracker t) {

javascript/ql/lib/semmle/javascript/Promises.qll

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,14 @@ module PromiseFlow {
425425
prop = errorProp() and
426426
pred = call.getCallback(0).getAReturn()
427427
)
428+
or
429+
// return from `async` function
430+
exists(DataFlow::FunctionNode f | f.getFunction().isAsync() |
431+
// ordinary return
432+
prop = valueProp() and
433+
pred = f.getAReturn() and
434+
succ = f.getReturnNode()
435+
)
428436
}
429437
}
430438

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { readFile } from 'fs/promises';
2+
3+
async function readFileUtf8(path: string): Promise<string> {
4+
return readFile(path, { encoding: 'utf8' });
5+
}
6+
7+
async function test(path: string) {
8+
await readFileUtf8(path); /* use (promised (return (member readFile (member exports (module fs/promises))))) */
9+
}

0 commit comments

Comments
 (0)