Skip to content

Commit 797aa9b

Browse files
committed
JS: model more implicit Node.js stream writes.
1 parent 47a76d6 commit 797aa9b

1 file changed

Lines changed: 161 additions & 1 deletion

File tree

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

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,107 @@ 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+
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+
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+
918+
overlay[local?]
919+
private predicate isWriteMethod(string name) { name = "write" }
920+
921+
overlay[local?]
922+
private predicate isWriteHook(string name) { name = "_write" }
923+
924+
overlay[local?]
925+
private ClassDefinition instantiatedClassWithoutWriteOverride(NewExpr creation) {
926+
DataFlow::localFlowStep*(TValueNode(result), TValueNode(creation.getCallee())) and
927+
not StreamMethodOverride<isWriteMethod/1>::overridesStreamMethod(result)
928+
}
929+
930+
overlay[local?]
931+
private NewExpr pipeDestinationWithoutWriteOverride(PipeCall pipe) {
932+
DataFlow::localFlowStep*(TValueNode(result), TValueNode(pipe.getArgument(0))) and
933+
not StreamMethodOverride<isWriteMethod/1>::overridesStreamInstanceMethod(result)
934+
}
935+
936+
/** Gets the implementation hook invoked by an inherited stream `write` method. */
937+
overlay[local?]
938+
private predicate pipeImplementationHook(PipeCall pipe, string hook) {
939+
exists(ClassDefinition c, NewExpr creation |
940+
creation = pipeDestinationWithoutWriteOverride(pipe) and
941+
c = instantiatedClassWithoutWriteOverride(creation)
942+
|
943+
WritableStreamClass<isWritableOrDuplex/1>::writableStreamClass(c) and
944+
hook = "_write"
945+
or
946+
WritableStreamClass<isTransform/1>::writableStreamClass(c) and
947+
if
948+
not StreamMethodOverride<isWriteHook/1>::overridesStreamMethod(c) and
949+
not StreamMethodOverride<isWriteHook/1>::overridesStreamInstanceMethod(creation)
950+
then hook = "_transform"
951+
else hook = "_write"
952+
)
953+
}
954+
955+
overlay[local?]
956+
private string pipeHookTag(string hook, string part) {
957+
hook = ["_write", "_transform"] and
958+
part = ["member", "call", "encoding", "callback"] and
959+
result = "nodejs.pipe." + hook + "." + part
960+
}
961+
861962
private DataFlow::SourceNode bufferInstance(DataFlow::TypeTracker t) {
862963
t.start() and
863964
exists(DataFlow::SourceNode constructor |
@@ -888,7 +989,14 @@ module NodeJSLib {
888989
override predicate needsSynthesizedNode(AstNode node, string tag, DataFlowCallable container) {
889990
(
890991
node instanceof PipeCall and
891-
tag = ["nodejs.pipe.write.member", "nodejs.pipe.write.call", "nodejs.pipe.write.chunk"]
992+
(
993+
tag = ["nodejs.pipe.write.member", "nodejs.pipe.write.call", "nodejs.pipe.write.chunk"]
994+
or
995+
exists(string hook |
996+
pipeImplementationHook(node, hook) and
997+
tag = pipeHookTag(hook, _)
998+
)
999+
)
8921000
or
8931001
node instanceof ReadableFromCall and
8941002
tag = "nodejs.readable.from.chunk"
@@ -973,6 +1081,8 @@ module NodeJSLib {
9731081
this =
9741082
getSynthesizedNode(pipe,
9751083
["nodejs.pipe.write.member", "nodejs.pipe.write.call", "nodejs.pipe.write.chunk"])
1084+
or
1085+
this = getSynthesizedNode(pipe, pipeHookTag(_, _))
9761086
}
9771087

9781088
override BasicBlock getBasicBlock() { result = pipe.getBasicBlock() }
@@ -1001,6 +1111,9 @@ module NodeJSLib {
10011111
getSynthesizedNode(any(PipeCall call),
10021112
["nodejs.pipe.write.member", "nodejs.pipe.write.chunk"])
10031113
or
1114+
this =
1115+
getSynthesizedNode(any(PipeCall call), pipeHookTag(_, ["member", "encoding", "callback"]))
1116+
or
10041117
this = getSynthesizedNode(any(ReadableFromCall call), "nodejs.readable.from.chunk")
10051118
}
10061119
}
@@ -1033,6 +1146,53 @@ module NodeJSLib {
10331146
override int getNumArgument() { result = 1 }
10341147
}
10351148

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

0 commit comments

Comments
 (0)