Skip to content

Commit 3c47288

Browse files
committed
JS: Support NodeJS pipe callbacks and related APIs.
1 parent cfbf52b commit 3c47288

1 file changed

Lines changed: 344 additions & 1 deletion

File tree

‎javascript/ql/lib/semmle/javascript/frameworks/NodeJSLib.qll‎

Lines changed: 344 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import javascript
66
import semmle.javascript.frameworks.HTTP
77
import semmle.javascript.security.SensitiveActions
8+
private import semmle.javascript.dataflow.internal.AdditionalFlowInternal
9+
private import semmle.javascript.dataflow.internal.DataFlowNode
10+
private import semmle.javascript.dataflow.internal.DataFlowPrivate
811
private import semmle.javascript.dataflow.internal.PreCallGraphStep
912

1013
module NodeJSLib {
@@ -656,6 +659,346 @@ module NodeJSLib {
656659
}
657660
}
658661

662+
signature predicate hasModuleNameSig(string name);
663+
664+
overlay[local?]
665+
private module BuiltinModule<hasModuleNameSig/1 hasModuleName> {
666+
/**
667+
* Holds if `node` represents a module import with a name
668+
* satisfying `hasModuleName`.
669+
*
670+
* This predicate does not depend on the call graph.
671+
*/
672+
predicate builtinModule(EarlyStageNode node) {
673+
exists(string name, Import imported |
674+
// A node module may be prefixed with `node:` (i.e., `node:fs` and `fs` is the
675+
// same module).
676+
hasModuleName(name) and imported.getImportedPathString() = ["", "node:"] + name
677+
|
678+
node = imported.getImportedModuleNode()
679+
or
680+
exists(ImportSpecifier spec, ImportDeclaration decl |
681+
decl = imported and
682+
spec = decl.getASpecifier() and
683+
not spec.isTypeOnly() and
684+
node = TValueNode(spec)
685+
|
686+
spec instanceof NamedImportSpecifier and
687+
spec.getImportedName() = "default"
688+
or
689+
spec instanceof ImportNamespaceSpecifier and
690+
decl.getASpecifier() instanceof ImportDefaultSpecifier
691+
)
692+
)
693+
or
694+
exists(EarlyStageNode pred |
695+
builtinModule(pred) and
696+
DataFlow::localFlowStep(pred, node)
697+
)
698+
}
699+
}
700+
701+
overlay[local?]
702+
private predicate isStream(string name) { name = "stream" }
703+
704+
overlay[local?]
705+
private EarlyStageNode getAStreamModuleNode() { BuiltinModule<isStream/1>::builtinModule(result) }
706+
707+
overlay[local?]
708+
private predicate isFs(string name) { name = "fs" }
709+
710+
overlay[local?]
711+
private EarlyStageNode getAFsModuleNode() { BuiltinModule<isFs/1>::builtinModule(result) }
712+
713+
overlay[local?]
714+
private predicate memberRead(EarlyStageNode base, string name, EarlyStageNode node) {
715+
exists(PropAccess access |
716+
base = TValueNode(access.getBase()) and
717+
name = access.getPropertyName() and
718+
node = TValueNode(access)
719+
)
720+
or
721+
exists(PropertyPattern prop |
722+
base = TValueNode(prop.getObjectPattern()) and
723+
name = prop.getName() and
724+
node = TPropNode(prop)
725+
)
726+
or
727+
exists(ImportDeclaration decl, NamedImportSpecifier spec |
728+
spec = decl.getASpecifier() and
729+
not spec.isTypeOnly() and
730+
base = TDestructuredModuleImportNode(decl) and
731+
name = spec.getImportedName() and
732+
node = TValueNode(spec)
733+
)
734+
}
735+
736+
/**
737+
* Holds if `node` refers to a built-in constructor for a readable Node.js
738+
* stream.
739+
*/
740+
overlay[local?]
741+
private predicate streamConstructor(EarlyStageNode node) {
742+
exists(EarlyStageNode base |
743+
base = getAStreamModuleNode() and
744+
memberRead(base, ["Readable", "Duplex", "Transform"], node)
745+
or
746+
base = getAFsModuleNode() and
747+
memberRead(base, "ReadStream", node)
748+
)
749+
or
750+
exists(EarlyStageNode pred |
751+
streamConstructor(pred) and
752+
DataFlow::localFlowStep(pred, node)
753+
)
754+
}
755+
756+
/**
757+
* Holds if `node` refers to the `from` factory of a built-in readable Node.js
758+
* stream constructor.
759+
*/
760+
overlay[local?]
761+
private predicate readableFromFactory(EarlyStageNode node) {
762+
exists(EarlyStageNode base |
763+
streamConstructor(base) and
764+
memberRead(base, "from", node)
765+
)
766+
or
767+
exists(EarlyStageNode pred |
768+
readableFromFactory(pred) and
769+
DataFlow::localFlowStep(pred, node)
770+
)
771+
}
772+
773+
/**
774+
* Holds if `node` refers to a built-in factory for a readable Node.js
775+
* stream.
776+
*
777+
* Also recognizes `fs.createReadStream` and the `from` and `fromWeb`
778+
* methods of built-in readable stream constructors.
779+
*/
780+
overlay[local?]
781+
private predicate streamFactory(EarlyStageNode node) {
782+
readableFromFactory(node)
783+
or
784+
exists(EarlyStageNode base |
785+
base = getAFsModuleNode() and
786+
memberRead(base, "createReadStream", node)
787+
or
788+
streamConstructor(base) and
789+
memberRead(base, "fromWeb", node)
790+
)
791+
or
792+
exists(EarlyStageNode pred |
793+
streamFactory(pred) and
794+
DataFlow::localFlowStep(pred, node)
795+
)
796+
}
797+
798+
overlay[local?]
799+
private class ReadableFromCall extends CallExpr {
800+
ReadableFromCall() {
801+
readableFromFactory(TValueNode(this.getCallee())) and
802+
exists(Expr e |
803+
e = this.getArgument(0) and
804+
not e instanceof SpreadElement and
805+
not this.getTopLevel().isExterns()
806+
)
807+
}
808+
}
809+
810+
overlay[local?]
811+
private string getAFluentStreamMethodName() {
812+
result =
813+
[
814+
"on", "once", "addListener", "prependListener", "prependOnceListener", "off",
815+
"removeListener", "removeAllListeners", "setMaxListeners", "setEncoding", "pause", "resume",
816+
"unpipe"
817+
]
818+
}
819+
820+
/**
821+
* Holds if `node` refers to a readable Node.js stream obtained from a
822+
* built-in constructor or factory.
823+
*
824+
* Also recognizes results of `pipe` calls, which return their destination,
825+
* to support chained pipes.
826+
*/
827+
overlay[local?]
828+
private predicate readableStream(EarlyStageNode node) {
829+
exists(InvokeExpr invoke | node = TValueNode(invoke) |
830+
streamConstructor(TValueNode(invoke.getCallee()))
831+
or
832+
invoke instanceof CallExpr and
833+
streamFactory(TValueNode(invoke.getCallee()))
834+
)
835+
or
836+
exists(EarlyStageNode pred |
837+
readableStream(pred) and
838+
DataFlow::localFlowStep(pred, node)
839+
)
840+
or
841+
exists(MethodCallExpr call |
842+
node = TValueNode(call) and
843+
readableStream(TValueNode(call.getReceiver())) and
844+
call.getMethodName() = getAFluentStreamMethodName()
845+
)
846+
or
847+
node = TValueNode(any(PipeCall call))
848+
}
849+
850+
overlay[local?]
851+
private class PipeCall extends MethodCallExpr {
852+
PipeCall() {
853+
this.getMethodName() = "pipe" and
854+
readableStream(TValueNode(this.getReceiver())) and
855+
exists(Expr e |
856+
e = this.getArgument(0) and
857+
not e instanceof SpreadElement and
858+
not this.getTopLevel().isExterns()
859+
)
860+
}
861+
}
862+
863+
overlay[local?]
864+
private class StreamFlowStep extends AdditionalFlowInternal {
865+
override predicate needsSynthesizedNode(AstNode node, string tag, DataFlowCallable container) {
866+
(
867+
node instanceof PipeCall and
868+
tag = ["nodejs.pipe.write.member", "nodejs.pipe.write.call", "nodejs.pipe.write.chunk"]
869+
or
870+
node instanceof ReadableFromCall and
871+
tag = "nodejs.readable.from.chunk"
872+
) and
873+
container.asSourceCallable() = node.getContainer()
874+
}
875+
876+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
877+
exists(MethodCallExpr call |
878+
readableStream(TValueNode(call.getReceiver())) and
879+
call.getMethodName() = getAFluentStreamMethodName() and
880+
pred = call.getReceiver().flow() and
881+
succ = call.flow()
882+
)
883+
or
884+
exists(PipeCall call |
885+
pred = call.getArgument(0).flow() and
886+
succ = call.flow()
887+
)
888+
}
889+
890+
override predicate readStep(
891+
DataFlow::Node pred, DataFlow::ContentSet contents, DataFlow::Node succ
892+
) {
893+
exists(ReadableFromCall call |
894+
pred = call.getArgument(0).flow() and
895+
contents =
896+
[
897+
DataFlow::ContentSet::arrayElement(), DataFlow::ContentSet::setElement(),
898+
DataFlow::ContentSet::iteratorElement()
899+
] and
900+
succ = getSynthesizedNode(call, "nodejs.readable.from.chunk")
901+
)
902+
or
903+
exists(PipeCall call |
904+
pred = call.getReceiver().flow() and
905+
contents = DataFlow::ContentSet::iteratorElement() and
906+
succ = getSynthesizedNode(call, "nodejs.pipe.write.chunk")
907+
)
908+
}
909+
910+
override predicate storeStep(
911+
DataFlow::Node pred, DataFlow::ContentSet contents, DataFlow::Node succ
912+
) {
913+
exists(ReadableFromCall call |
914+
pred = getSynthesizedNode(call, "nodejs.readable.from.chunk") and
915+
contents = DataFlow::ContentSet::iteratorElement() and
916+
succ = call.flow()
917+
)
918+
}
919+
}
920+
921+
overlay[local?]
922+
private class ReadableFromChunk extends GenericSynthesizedNode {
923+
ReadableFromCall call;
924+
925+
ReadableFromChunk() { this = getSynthesizedNode(call, "nodejs.readable.from.chunk") }
926+
927+
override BasicBlock getBasicBlock() { result = call.getBasicBlock() }
928+
929+
override File getFile() { result = call.getFile() }
930+
931+
override Expr getEnclosingExpr() { result = call }
932+
}
933+
934+
overlay[local?]
935+
private class PipeNode extends GenericSynthesizedNode {
936+
PipeCall pipe;
937+
938+
PipeNode() {
939+
this =
940+
getSynthesizedNode(pipe,
941+
["nodejs.pipe.write.member", "nodejs.pipe.write.call", "nodejs.pipe.write.chunk"])
942+
}
943+
944+
override BasicBlock getBasicBlock() { result = pipe.getBasicBlock() }
945+
946+
override File getFile() { result = pipe.getFile() }
947+
948+
override Expr getEnclosingExpr() { result = pipe }
949+
}
950+
951+
overlay[local?]
952+
private class PipeWriteMember extends PipeNode, DataFlow::PropRead {
953+
PipeWriteMember() { this = getSynthesizedNode(pipe, "nodejs.pipe.write.member") }
954+
955+
override DataFlow::Node getBase() { result = pipe.getArgument(0).flow() }
956+
957+
override string getPropertyName() { result = "write" }
958+
959+
override Expr getPropertyNameExpr() { none() }
960+
}
961+
962+
/** Registers local sources separately to avoid a cycle with the property-read hierarchy. */
963+
overlay[local?]
964+
private class StreamSourceNodes extends DataFlow::SourceNode::Range {
965+
StreamSourceNodes() {
966+
this =
967+
getSynthesizedNode(any(PipeCall call),
968+
["nodejs.pipe.write.member", "nodejs.pipe.write.chunk"])
969+
or
970+
this = getSynthesizedNode(any(ReadableFromCall call), "nodejs.readable.from.chunk")
971+
}
972+
}
973+
974+
/** An implicit `destination.write(chunk)` call at the explicit pipe's location. */
975+
overlay[local?]
976+
private class PipeWriteCall extends PipeNode, DataFlow::Impl::MethodCallNodeDef {
977+
PipeWriteCall() { this = getSynthesizedNode(pipe, "nodejs.pipe.write.call") }
978+
979+
override InvokeExpr getInvokeExpr() { result = pipe }
980+
981+
override string getCalleeName() { result = "write" }
982+
983+
override string getMethodName() { result = "write" }
984+
985+
override DataFlow::Node getCalleeNode() {
986+
result = getSynthesizedNode(pipe, "nodejs.pipe.write.member")
987+
}
988+
989+
override DataFlow::Node getReceiver() { result = pipe.getArgument(0).flow() }
990+
991+
override DataFlow::Node getArgument(int index) {
992+
index = 0 and result = getSynthesizedNode(pipe, "nodejs.pipe.write.chunk")
993+
}
994+
995+
override DataFlow::Node getAnArgument() { result = this.getArgument(0) }
996+
997+
override DataFlow::Node getASpreadArgument() { none() }
998+
999+
override int getNumArgument() { result = 1 }
1000+
}
1001+
6591002
/**
6601003
* A write to the file system, using a stream.
6611004
*/
@@ -695,7 +1038,7 @@ module NodeJSLib {
6951038
result = this
6961039
or
6971040
method = "pipe" and
698-
result = this.getArgument(0)
1041+
result = getSynthesizedNode(this.getInvokeExpr(), "nodejs.pipe.write.chunk")
6991042
or
7001043
method = EventEmitter::on() and
7011044
this.getArgument(0).mayHaveStringValue("data") and

0 commit comments

Comments
 (0)