Skip to content

Commit c8b8a28

Browse files
authored
Merge pull request #7119 from github/max-schaefer/api-graphs-property-copies
Approved by asgerf
2 parents c17560f + a8c4455 commit c8b8a28

File tree

3 files changed

+92
-28
lines changed

3 files changed

+92
-28
lines changed

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

Lines changed: 75 additions & 28 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
@@ -556,6 +556,32 @@ module API {
556556
nd = MkDef(rhs)
557557
}
558558

559+
/**
560+
* Holds if `ref` is a read of a property described by `lbl` on `pred`, and
561+
* `propDesc` is compatible with that property, meaning it is either the
562+
* name of the property itself or the empty string.
563+
*/
564+
pragma[noinline]
565+
private predicate propertyRead(
566+
DataFlow::SourceNode pred, string propDesc, string lbl, DataFlow::Node ref
567+
) {
568+
ref = pred.getAPropertyRead() and
569+
lbl = Label::memberFromRef(ref) and
570+
(
571+
lbl = Label::member(propDesc)
572+
or
573+
propDesc = ""
574+
)
575+
or
576+
PromiseFlow::loadStep(pred.getALocalUse(), ref, Promises::valueProp()) and
577+
lbl = Label::promised() and
578+
(propDesc = Promises::valueProp() or propDesc = "")
579+
or
580+
PromiseFlow::loadStep(pred.getALocalUse(), ref, Promises::errorProp()) and
581+
lbl = Label::promisedError() and
582+
(propDesc = Promises::errorProp() or propDesc = "")
583+
}
584+
559585
/**
560586
* Holds if `ref` is a use of a node that should have an incoming edge from `base` labeled
561587
* `lbl` in the API graph.
@@ -567,26 +593,25 @@ module API {
567593
base = MkRoot() and
568594
ref = lbl.(EntryPoint).getAUse()
569595
or
596+
// property reads
597+
exists(DataFlow::SourceNode src, DataFlow::SourceNode pred, string propDesc |
598+
use(base, src) and
599+
pred = trackUseNode(src, false, 0, propDesc) and
600+
propertyRead(pred, propDesc, lbl, ref) and
601+
// `module.exports` is special: it is a use of a def-node, not a use-node,
602+
// so we want to exclude it here
603+
(base instanceof TNonModuleDef or base instanceof TUse)
604+
)
605+
or
606+
// invocations
570607
exists(DataFlow::SourceNode src, DataFlow::SourceNode pred |
571608
use(base, src) and pred = trackUseNode(src)
572609
|
573-
// `module.exports` is special: it is a use of a def-node, not a use-node,
574-
// so we want to exclude it here
575-
(base instanceof TNonModuleDef or base instanceof TUse) and
576-
lbl = Label::memberFromRef(ref) and
577-
ref = pred.getAPropertyRead()
578-
or
579610
lbl = Label::instance() and
580611
ref = pred.getAnInstantiation()
581612
or
582613
lbl = Label::return() and
583614
ref = pred.getAnInvocation()
584-
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())
590615
)
591616
or
592617
exists(DataFlow::Node def, DataFlow::FunctionNode fn |
@@ -680,36 +705,58 @@ module API {
680705
)
681706
}
682707

708+
private import semmle.javascript.dataflow.TypeTracking
709+
683710
/**
684711
* Gets a data-flow node to which `nd`, which is a use of an API-graph node, flows.
685712
*
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.)
713+
* The flow from `nd` to that node may be inter-procedural, and is further described by three
714+
* flags:
715+
*
716+
* - `promisified`: if true `true`, the flow goes through a promisification;
717+
* - `boundArgs`: for function values, tracks how many arguments have been bound throughout
718+
* the flow. To ensure termination, we somewhat arbitrarily constrain the number of bound
719+
* arguments to be at most ten.
720+
* - `prop`: if non-empty, the flow is only guaranteed to preserve the value of this property,
721+
* and not necessarily the entire object.
690722
*/
691723
private DataFlow::SourceNode trackUseNode(
692-
DataFlow::SourceNode nd, boolean promisified, int boundArgs, DataFlow::TypeTracker t
724+
DataFlow::SourceNode nd, boolean promisified, int boundArgs, string prop,
725+
DataFlow::TypeTracker t
693726
) {
694727
t.start() and
695728
use(_, nd) and
696729
result = nd and
697730
promisified = false and
698-
boundArgs = 0
731+
boundArgs = 0 and
732+
prop = ""
699733
or
700734
exists(Promisify::PromisifyCall promisify |
701-
trackUseNode(nd, false, boundArgs, t.continue()).flowsTo(promisify.getArgument(0)) and
735+
trackUseNode(nd, false, boundArgs, prop, t.continue()).flowsTo(promisify.getArgument(0)) and
702736
promisified = true and
737+
prop = "" and
703738
result = promisify
704739
)
705740
or
706741
exists(DataFlow::PartialInvokeNode pin, DataFlow::Node pred, int predBoundArgs |
707-
trackUseNode(nd, promisified, predBoundArgs, t.continue()).flowsTo(pred) and
742+
trackUseNode(nd, promisified, predBoundArgs, prop, t.continue()).flowsTo(pred) and
743+
prop = "" and
708744
result = pin.getBoundFunction(pred, boundArgs - predBoundArgs) and
709745
boundArgs in [0 .. 10]
710746
)
711747
or
712-
t = useStep(nd, promisified, boundArgs, result)
748+
exists(DataFlow::Node pred, string preprop |
749+
trackUseNode(nd, promisified, boundArgs, preprop, t.continue()).flowsTo(pred) and
750+
promisified = false and
751+
boundArgs = 0 and
752+
SharedTypeTrackingStep::loadStoreStep(pred, result, prop)
753+
|
754+
prop = preprop
755+
or
756+
preprop = ""
757+
)
758+
or
759+
t = useStep(nd, promisified, boundArgs, prop, result)
713760
}
714761

715762
private import semmle.javascript.dataflow.internal.StepSummary
@@ -723,27 +770,27 @@ module API {
723770
*/
724771
pragma[noopt]
725772
private DataFlow::TypeTracker useStep(
726-
DataFlow::Node nd, boolean promisified, int boundArgs, DataFlow::Node res
773+
DataFlow::Node nd, boolean promisified, int boundArgs, string prop, DataFlow::Node res
727774
) {
728775
exists(DataFlow::TypeTracker t, StepSummary summary, DataFlow::SourceNode prev |
729-
prev = trackUseNode(nd, promisified, boundArgs, t) and
776+
prev = trackUseNode(nd, promisified, boundArgs, prop, t) and
730777
StepSummary::step(prev, res, summary) and
731778
result = t.append(summary)
732779
)
733780
}
734781

735782
private DataFlow::SourceNode trackUseNode(
736-
DataFlow::SourceNode nd, boolean promisified, int boundArgs
783+
DataFlow::SourceNode nd, boolean promisified, int boundArgs, string prop
737784
) {
738-
result = trackUseNode(nd, promisified, boundArgs, DataFlow::TypeTracker::end())
785+
result = trackUseNode(nd, promisified, boundArgs, prop, DataFlow::TypeTracker::end())
739786
}
740787

741788
/**
742789
* Gets a node that is inter-procedurally reachable from `nd`, which is a use of some node.
743790
*/
744791
cached
745792
DataFlow::SourceNode trackUseNode(DataFlow::SourceNode nd) {
746-
result = trackUseNode(nd, false, 0)
793+
result = trackUseNode(nd, false, 0, "")
747794
}
748795

749796
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)