Skip to content

Commit 43c7ddc

Browse files
committed
JS: model more implicit Node.js stream writes.
1 parent f363878 commit 43c7ddc

1 file changed

Lines changed: 154 additions & 1 deletion

File tree

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

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,100 @@ module NodeJSLib {
858858
}
859859
}
860860

861+
signature predicate isSuperClassSig(ClassDefinition clazz);
862+
863+
overlay[local?]
864+
private module StreamSuperClass<isSuperClassSig/1 isSuperClass> {
865+
/** Gets a relevant super class of `subclass`. */
866+
ClassDefinition streamSuperClass(ClassDefinition subclass) {
867+
isSuperClass(result) and
868+
DataFlow::localFlowStep*(TValueNode(result), TValueNode(subclass.getSuperClass()))
869+
}
870+
}
871+
872+
signature predicate hasStreamKindSig(string kind);
873+
874+
overlay[local?]
875+
private module WritableStreamClass<hasStreamKindSig/1 hasStreamKind> {
876+
/**
877+
* Holds if `c` extends a built-in Node.js stream class whose name
878+
* satisfies `hasStreamKind`.
879+
*/
880+
predicate writableStreamClass(ClassDefinition c) {
881+
exists(string kind, EarlyStageNode constructor |
882+
hasStreamKind(kind) and
883+
memberRead(getAStreamModuleNode(), kind, constructor) and
884+
DataFlow::localFlowStep*(constructor, TValueNode(c.getSuperClass()))
885+
)
886+
or
887+
exists(StreamSuperClass<writableStreamClass/1>::streamSuperClass(c))
888+
}
889+
}
890+
891+
overlay[local?]
892+
private predicate isWritableOrDuplex(string kind) { kind = ["Writable", "Duplex"] }
893+
894+
overlay[local?]
895+
private predicate isTransform(string kind) { kind = "Transform" }
896+
897+
signature predicate hasStreamMethodNameSig(string name);
898+
899+
overlay[local?]
900+
private module StreamMethodOverride<hasStreamMethodNameSig/1 hasStreamMethodName> {
901+
private predicate overridesStreamMethod(ClassDefinition c) {
902+
exists(string name, MemberDeclaration member |
903+
hasStreamMethodName(name) and
904+
member = StreamSuperClass<overridesStreamMethod/1>::streamSuperClass*(c).getMember(name) and
905+
not member.isStatic()
906+
)
907+
}
908+
909+
private predicate overridesStreamInstanceMethod(NewExpr creation) {
910+
exists(Assignment assignment, PropAccess property |
911+
property = assignment.getLhs() and
912+
hasStreamMethodName(property.getPropertyName()) and
913+
DataFlow::localFlowStep*(TValueNode(creation), TValueNode(property.getBase()))
914+
)
915+
}
916+
917+
predicate hasStreamMethodOverride(ClassDefinition clazz, NewExpr creation) {
918+
overridesStreamMethod(clazz)
919+
or
920+
overridesStreamInstanceMethod(creation)
921+
}
922+
}
923+
924+
overlay[local?]
925+
private predicate isWriteMethod(string name) { name = "write" }
926+
927+
overlay[local?]
928+
private predicate isWriteHook(string name) { name = "_write" }
929+
930+
/** Gets the implementation hook invoked by an inherited stream `write` method. */
931+
overlay[local?]
932+
private predicate pipeImplementationHook(PipeCall pipe, string hook) {
933+
exists(ClassDefinition c, NewExpr creation |
934+
DataFlow::localFlowStep*(TValueNode(c), TValueNode(creation.getCallee())) and
935+
DataFlow::localFlowStep*(TValueNode(creation), TValueNode(pipe.getArgument(0))) and
936+
not StreamMethodOverride<isWriteMethod/1>::hasStreamMethodOverride(c, creation)
937+
|
938+
WritableStreamClass<isWritableOrDuplex/1>::writableStreamClass(c) and
939+
hook = "_write"
940+
or
941+
WritableStreamClass<isTransform/1>::writableStreamClass(c) and
942+
if not StreamMethodOverride<isWriteHook/1>::hasStreamMethodOverride(c, creation)
943+
then hook = "_transform"
944+
else hook = "_write"
945+
)
946+
}
947+
948+
overlay[local?]
949+
private string pipeHookTag(string hook, string part) {
950+
hook = ["_write", "_transform"] and
951+
part = ["member", "call", "encoding", "callback"] and
952+
result = "nodejs.pipe." + hook + "." + part
953+
}
954+
861955
private DataFlow::SourceNode bufferInstance(DataFlow::TypeTracker t) {
862956
t.start() and
863957
exists(DataFlow::SourceNode constructor |
@@ -888,7 +982,14 @@ module NodeJSLib {
888982
override predicate needsSynthesizedNode(AstNode node, string tag, DataFlowCallable container) {
889983
(
890984
node instanceof PipeCall and
891-
tag = ["nodejs.pipe.write.member", "nodejs.pipe.write.call", "nodejs.pipe.write.chunk"]
985+
(
986+
tag = ["nodejs.pipe.write.member", "nodejs.pipe.write.call", "nodejs.pipe.write.chunk"]
987+
or
988+
exists(string hook |
989+
pipeImplementationHook(node, hook) and
990+
tag = pipeHookTag(hook, _)
991+
)
992+
)
892993
or
893994
node instanceof ReadableFromCall and
894995
tag = "nodejs.readable.from.chunk"
@@ -973,6 +1074,8 @@ module NodeJSLib {
9731074
this =
9741075
getSynthesizedNode(pipe,
9751076
["nodejs.pipe.write.member", "nodejs.pipe.write.call", "nodejs.pipe.write.chunk"])
1077+
or
1078+
this = getSynthesizedNode(pipe, pipeHookTag(_, _))
9761079
}
9771080

9781081
override BasicBlock getBasicBlock() { result = pipe.getBasicBlock() }
@@ -1001,6 +1104,9 @@ module NodeJSLib {
10011104
getSynthesizedNode(any(PipeCall call),
10021105
["nodejs.pipe.write.member", "nodejs.pipe.write.chunk"])
10031106
or
1107+
this =
1108+
getSynthesizedNode(any(PipeCall call), pipeHookTag(_, ["member", "encoding", "callback"]))
1109+
or
10041110
this = getSynthesizedNode(any(ReadableFromCall call), "nodejs.readable.from.chunk")
10051111
}
10061112
}
@@ -1033,6 +1139,53 @@ module NodeJSLib {
10331139
override int getNumArgument() { result = 1 }
10341140
}
10351141

1142+
overlay[local?]
1143+
private class PipeHookMember extends PipeNode, DataFlow::PropRead {
1144+
string hook;
1145+
1146+
PipeHookMember() { this = getSynthesizedNode(pipe, pipeHookTag(hook, "member")) }
1147+
1148+
override DataFlow::Node getBase() { result = pipe.getArgument(0).flow() }
1149+
1150+
override string getPropertyName() { result = hook }
1151+
1152+
override Expr getPropertyNameExpr() { none() }
1153+
}
1154+
1155+
/** An implicit `_write` or `_transform` invocation made by the native `write` method. */
1156+
overlay[local?]
1157+
private class PipeHookCall extends PipeNode, DataFlow::Impl::MethodCallNodeDef {
1158+
string hook;
1159+
1160+
PipeHookCall() { this = getSynthesizedNode(pipe, pipeHookTag(hook, "call")) }
1161+
1162+
override InvokeExpr getInvokeExpr() { result = pipe }
1163+
1164+
override string getCalleeName() { result = hook }
1165+
1166+
override string getMethodName() { result = hook }
1167+
1168+
override DataFlow::Node getCalleeNode() {
1169+
result = getSynthesizedNode(pipe, pipeHookTag(hook, "member"))
1170+
}
1171+
1172+
override DataFlow::Node getReceiver() { result = pipe.getArgument(0).flow() }
1173+
1174+
override DataFlow::Node getArgument(int index) {
1175+
index = 0 and result = getSynthesizedNode(pipe, "nodejs.pipe.write.chunk")
1176+
or
1177+
index = 1 and result = getSynthesizedNode(pipe, pipeHookTag(hook, "encoding"))
1178+
or
1179+
index = 2 and result = getSynthesizedNode(pipe, pipeHookTag(hook, "callback"))
1180+
}
1181+
1182+
override DataFlow::Node getAnArgument() { result = this.getArgument(_) }
1183+
1184+
override DataFlow::Node getASpreadArgument() { none() }
1185+
1186+
override int getNumArgument() { result = 3 }
1187+
}
1188+
10361189
/**
10371190
* A write to the file system, using a stream.
10381191
*/

0 commit comments

Comments
 (0)