Skip to content

Commit b75486b

Browse files
committed
JS: refactor NoSQL::Mongoose. Introduce Mongoose::CommonInterface
1 parent 833d1b1 commit b75486b

1 file changed

Lines changed: 127 additions & 106 deletions

File tree

  • javascript/ql/src/semmle/javascript/frameworks

javascript/ql/src/semmle/javascript/frameworks/NoSQL.qll

Lines changed: 127 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,49 @@ private module Mongoose {
209209
result = getAMongooseInstance().getAMemberCall("createConnection")
210210
}
211211

212+
/**
213+
* A common interface for various Mongoose calls.
214+
*/
215+
private class CommonInterface extends DataFlow::InvokeNode {
216+
/**
217+
* Holds if this call returns an object of type `Query`.
218+
*/
219+
abstract predicate returnsQuery();
220+
221+
/**
222+
* Holds if this call returns a `Query` that evaluates to a one or
223+
* more Documents (`asArray` is false if it evaluates to a single
224+
* Document).
225+
*/
226+
abstract predicate returnsDocumentQuery(boolean asArray);
227+
228+
/**
229+
* Holds if this call interprets `arg` as a query.
230+
*/
231+
abstract predicate interpretsArgumentAsQuery(DataFlow::Node arg);
232+
}
233+
212234
/**
213235
* Provides classes modeling the Mongoose Model class
214236
*/
215237
module Model {
238+
private class ModelAsCommonInterface extends CommonInterface, DataFlow::MethodCallNode {
239+
ModelAsCommonInterface() { this = ref().getAMethodCall() }
240+
241+
override predicate returnsQuery() { MethodSignatures::returnsQuery(getMethodName()) }
242+
243+
override predicate returnsDocumentQuery(boolean asArray) {
244+
MethodSignatures::returnsDocumentQuery(getMethodName(), asArray)
245+
}
246+
247+
override predicate interpretsArgumentAsQuery(DataFlow::Node arg) {
248+
exists(int n |
249+
MethodSignatures::interpretsArgumentAsQuery(this.getMethodName(), n) and
250+
arg = this.getArgument(n)
251+
)
252+
}
253+
}
254+
216255
/**
217256
* Gets a data flow node referring to a Mongoose Model object.
218257
*/
@@ -279,10 +318,11 @@ private module Mongoose {
279318
}
280319

281320
/**
282-
* Holds if Model method `name` returns one or more documents, the
283-
* documents are wrapped in an array if `asArray` is true.
321+
* Holds if Document method `name` returns a query that results in
322+
* one or more documents, the documents are wrapped in an array
323+
* if `asArray` is true.
284324
*/
285-
predicate returnsDocument(string name, boolean asArray) {
325+
predicate returnsDocumentQuery(string name, boolean asArray) {
286326
asArray = false and name = "findOne"
287327
or
288328
asArray = true and name = "find"
@@ -294,47 +334,41 @@ private module Mongoose {
294334
* Provides classes modeling the Mongoose Query class
295335
*/
296336
module Query {
297-
/**
298-
* A Mongoose query object as a result of a Model method call.
299-
*/
300-
private class QueryFromModel extends DataFlow::MethodCallNode {
301-
QueryFromModel() {
302-
exists(string name |
303-
Model::MethodSignatures::returnsQuery(name) and
304-
Model::ref().getAMethodCall(name) = this
305-
)
337+
private class QueryAsCommonInterface extends CommonInterface, DataFlow::MethodCallNode {
338+
QueryAsCommonInterface() { this = ref().getAMethodCall() }
339+
340+
override predicate returnsQuery() { MethodSignatures::returnsQuery(getMethodName()) }
341+
342+
override predicate returnsDocumentQuery(boolean asArray) {
343+
MethodSignatures::returnsDocumentQuery(getMethodName(), asArray)
306344
}
307-
}
308345

309-
/**
310-
* A Mongoose query object as a result of a Document method call.
311-
*/
312-
private class QueryFromDocument extends DataFlow::MethodCallNode {
313-
QueryFromDocument() {
314-
exists(string name |
315-
Document::MethodSignatures::returnsQuery(name) and
316-
Document::ref().getAMethodCall(name) = this
346+
override predicate interpretsArgumentAsQuery(DataFlow::Node arg) {
347+
exists(int n |
348+
MethodSignatures::interpretsArgumentAsQuery(this.getMethodName(), n) and
349+
arg = this.getArgument(n)
317350
)
318351
}
319352
}
320353

321-
/**
322-
* A Mongoose query object as a result of a Query constructor invocation.
323-
*/
324-
class QueryFromConstructor extends DataFlow::NewNode {
325-
QueryFromConstructor() {
354+
private class NewQueryAsCommonInterface extends CommonInterface {
355+
NewQueryAsCommonInterface() {
326356
this = getAMongooseInstance().getAPropertyRead("Query").getAnInstantiation()
327357
}
358+
359+
override predicate returnsQuery() { any() }
360+
361+
override predicate returnsDocumentQuery(boolean asArray) { none() }
362+
363+
override predicate interpretsArgumentAsQuery(DataFlow::Node arg) { arg = this.getArgument(2) }
328364
}
329365

330366
/**
331367
* Gets a data flow node referring to a Mongoose query object.
332368
*/
333369
private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
334370
(
335-
result instanceof QueryFromConstructor or
336-
result instanceof QueryFromModel or
337-
result instanceof QueryFromDocument or
371+
result.(CommonInterface).returnsQuery() or
338372
result.hasUnderlyingType("mongoose", "Query")
339373
) and
340374
t.start()
@@ -475,10 +509,11 @@ private module Mongoose {
475509
}
476510

477511
/**
478-
* Holds if Query method `name` returns one or more documents, the
479-
* documents are wrapped in an array if `asArray` is true.
512+
* Holds if Query method `name` returns a query that results in
513+
* one or more documents, the documents are wrapped in an array
514+
* if `asArray` is true.
480515
*/
481-
predicate returnsDocument(string name, boolean asArray) {
516+
predicate returnsDocumentQuery(string name, boolean asArray) {
482517
asArray = false and name = "findOne"
483518
or
484519
asArray = true and name = "find"
@@ -490,20 +525,31 @@ private module Mongoose {
490525
* Provides classes modeling the Mongoose Document class
491526
*/
492527
module Document {
528+
private class DocumentAsCommonInterface extends CommonInterface, DataFlow::MethodCallNode {
529+
DocumentAsCommonInterface() { this = ref().getAMethodCall() }
530+
531+
override predicate returnsQuery() { MethodSignatures::returnsQuery(getMethodName()) }
532+
533+
override predicate returnsDocumentQuery(boolean asArray) {
534+
MethodSignatures::returnsDocumentQuery(getMethodName(), asArray)
535+
}
536+
537+
override predicate interpretsArgumentAsQuery(DataFlow::Node arg) {
538+
exists(int n |
539+
MethodSignatures::interpretsArgumentAsQuery(this.getMethodName(), n) and
540+
arg = this.getArgument(n)
541+
)
542+
}
543+
}
544+
493545
/**
494546
* A Mongoose Document that is retrieved from the backing database.
495547
*/
496548
class RetrievedDocument extends DataFlow::SourceNode {
497549
RetrievedDocument() {
498550
exists(boolean asArray, DataFlow::ParameterNode param |
499-
exists(DataFlow::SourceNode base, DataFlow::MethodCallNode call, string name |
500-
base = Query::ref() and Query::MethodSignatures::returnsDocument(name, asArray)
501-
or
502-
base = Model::ref() and Model::MethodSignatures::returnsDocument(name, asArray)
503-
or
504-
base = ref() and MethodSignatures::returnsDocument(name, asArray)
505-
|
506-
call = base.getAMethodCall(name) and
551+
exists(CommonInterface call |
552+
call.returnsDocumentQuery(asArray) and
507553
param = call.getCallback(call.getNumArgument() - 1).getParameter(1)
508554
)
509555
or
@@ -520,7 +566,7 @@ private module Mongoose {
520566
param = call.getCallback(0).getParameter(paramIndex) and
521567
exists(DataFlow::MethodCallNode pred |
522568
// limitation: look at the previous method call
523-
Query::MethodSignatures::returnsDocument(pred.getMethodName(), asArray) and
569+
Query::MethodSignatures::returnsDocumentQuery(pred.getMethodName(), asArray) and
524570
pred.getAMethodCall() = call
525571
)
526572
)
@@ -551,8 +597,7 @@ private module Mongoose {
551597
exists(DataFlow::TypeTracker t2, DataFlow::SourceNode succ | succ = ref(t2) |
552598
result = succ.track(t2, t)
553599
or
554-
result =
555-
succ.getAMethodCall(any(string name | MethodSignatures::returnsDocument(name, true))) and
600+
result = succ.getAMethodCall(any(string name | MethodSignatures::returnsDocument(name))) and
556601
t = t2.continue()
557602
)
558603
}
@@ -562,7 +607,7 @@ private module Mongoose {
562607
*/
563608
DataFlow::SourceNode ref() { result = ref(DataFlow::TypeTracker::end()) }
564609

565-
module MethodSignatures {
610+
private module MethodSignatures {
566611
/**
567612
* Holds if Document method `name` returns a Query.
568613
*/
@@ -590,20 +635,23 @@ private module Mongoose {
590635
}
591636

592637
/**
593-
* Holds if Document method `name` returns one or more documents, the
594-
* documents are wrapped in an array if `asArray` is true.
638+
* Holds if Document method `name` returns a query that results in
639+
* one or more documents, the documents are wrapped in an array
640+
* if `asArray` is true.
595641
*/
596-
predicate returnsDocument(string name, boolean asArray) {
642+
predicate returnsDocumentQuery(string name, boolean asArray) {
597643
// Documents are subtypes of Models
598-
Model::MethodSignatures::returnsDocument(name, asArray)
599-
or
600-
asArray = false and
601-
(
602-
name = "depopulate" or
603-
name = "init" or
604-
name = "populate" or
605-
name = "overwrite"
606-
)
644+
Model::MethodSignatures::returnsDocumentQuery(name, asArray)
645+
}
646+
647+
/**
648+
* Holds if Document method `name` returns a Document.
649+
*/
650+
predicate returnsDocument(string name) {
651+
name = "depopulate" or
652+
name = "init" or
653+
name = "populate" or
654+
name = "overwrite"
607655
}
608656
}
609657
}
@@ -629,66 +677,39 @@ private module Mongoose {
629677
* An expression that is interpreted as a (part of a) MongoDB query.
630678
*/
631679
class MongoDBQueryPart extends NoSQL::Query {
632-
MongoDBQueryPart() {
633-
exists(DataFlow::MethodCallNode mcn, string method, int n |
634-
Model::MethodSignatures::interpretsArgumentAsQuery(method, n) and
635-
mcn = Model::ref().getAMethodCall(method) and
636-
this = mcn.getArgument(n).asExpr()
637-
)
638-
or
639-
this = any(Query::QueryFromConstructor c).getArgument(2).asExpr()
640-
or
641-
exists(string method, int n | Query::MethodSignatures::interpretsArgumentAsQuery(method, n) |
642-
this = Query::ref().getAMethodCall(method).getArgument(n).asExpr()
643-
)
644-
or
645-
exists(DataFlow::MethodCallNode mcn, string method, int n |
646-
Document::MethodSignatures::interpretsArgumentAsQuery(method, n) and
647-
mcn = Document::ref().getAMethodCall(method) and
648-
this = mcn.getArgument(n).asExpr()
649-
)
650-
}
680+
MongoDBQueryPart() { any(CommonInterface call).interpretsArgumentAsQuery(this.flow()) }
651681
}
652682

653683
/**
654684
* An evaluation of a MongoDB query.
655685
*/
656-
class MongoDBQueryEvaluation extends DatabaseAccess {
657-
DataFlow::MethodCallNode mcn;
686+
class ShorthandQueryEvaluation extends DatabaseAccess {
687+
CommonInterface common;
688+
689+
ShorthandQueryEvaluation() {
690+
this = common and
691+
// shorthand for execution: provide a callback
692+
common.returnsQuery() and
693+
exists(common.getCallback(common.getNumArgument() - 1))
694+
}
658695

659-
MongoDBQueryEvaluation() {
660-
this = mcn and
661-
(
662-
exists(string method |
663-
Model::MethodSignatures::returnsQuery(method) and
664-
mcn = Model::ref().getAMethodCall(method) and
665-
// callback provided to a Model method call
666-
exists(mcn.getCallback(mcn.getNumArgument() - 1))
667-
)
668-
or
669-
Query::ref().getAMethodCall() = mcn and
670-
(
671-
// explicit execution using a Query method call
672-
exists(string executor | executor = "exec" or executor = "then" or executor = "catch" |
673-
mcn.getMethodName() = executor
674-
)
675-
or
676-
// callback provided to a Query method call
677-
exists(mcn.getCallback(mcn.getNumArgument() - 1))
678-
)
679-
or
680-
exists(string method |
681-
Document::MethodSignatures::returnsQuery(method) and
682-
mcn = Document::ref().getAMethodCall(method) and
683-
// callback provided to a Document method call
684-
exists(mcn.getCallback(mcn.getNumArgument() - 1))
685-
)
696+
override DataFlow::Node getAQueryArgument() {
697+
// NB: the complete information is not easily accessible for deeply chained calls
698+
common.interpretsArgumentAsQuery(result)
699+
}
700+
}
701+
702+
class ExplicitQueryEvaluation extends DatabaseAccess {
703+
ExplicitQueryEvaluation() {
704+
// explicit execution using a Query method call
705+
exists(string executor | executor = "exec" or executor = "then" or executor = "catch" |
706+
Query::ref().getAMethodCall(executor) = this
686707
)
687708
}
688709

689710
override DataFlow::Node getAQueryArgument() {
690-
// NB: this does not account for all of the chained calls leading to this execution
691-
mcn.getAnArgument().asExpr().(MongoDBQueryPart).flow() = result
711+
// NB: the complete information is not easily accessible for deeply chained calls
712+
none()
692713
}
693714
}
694715
}

0 commit comments

Comments
 (0)