Skip to content

Commit deb20fc

Browse files
authored
Merge pull request github#3076 from esbena/js/even-more-mongoose-improvements
Approved by erik-krogh
2 parents 2821b01 + b1a722f commit deb20fc

File tree

4 files changed

+345
-54
lines changed

4 files changed

+345
-54
lines changed

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

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

212+
/**
213+
* A Mongoose function invocation.
214+
*/
215+
private class InvokeNode extends DataFlow::InvokeNode {
216+
/**
217+
* Holds if this invocation returns an object of type `Query`.
218+
*/
219+
abstract predicate returnsQuery();
220+
221+
/**
222+
* Holds if this invocation returns a `Query` that evaluates to 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 invocation 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 ModelInvokeNode extends InvokeNode, DataFlow::MethodCallNode {
239+
ModelInvokeNode() { 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
*/
219258
private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
220259
(
221-
result = getAMongooseInstance().getAMemberCall("model") or
260+
result = getAMongooseInstance().getAMemberCall("model")
261+
or
262+
exists(DataFlow::SourceNode conn | conn = createConnection() |
263+
result = conn.getAMemberCall("model") or
264+
result = conn.getAPropertyRead("models").getAPropertyRead()
265+
)
266+
or
222267
result.hasUnderlyingType("mongoose", "Model")
223268
) and
224269
t.start()
@@ -271,41 +316,59 @@ private module Mongoose {
271316
name = "updateOne" or
272317
name = "where"
273318
}
319+
320+
/**
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.
324+
*/
325+
predicate returnsDocumentQuery(string name, boolean asArray) {
326+
asArray = false and name = "findOne"
327+
or
328+
asArray = true and name = "find"
329+
}
274330
}
275331
}
276332

277333
/**
278334
* Provides classes modeling the Mongoose Query class
279335
*/
280336
module Query {
281-
/**
282-
* A Mongoose query object as a result of a Model method call.
283-
*/
284-
private class QueryFromModel extends DataFlow::MethodCallNode {
285-
QueryFromModel() {
286-
exists(string name |
287-
Model::MethodSignatures::returnsQuery(name) and
288-
Model::ref().getAMethodCall(name) = this
337+
private class QueryInvokeNode extends InvokeNode, DataFlow::MethodCallNode {
338+
QueryInvokeNode() { this = ref().getAMethodCall() }
339+
340+
override predicate returnsQuery() { MethodSignatures::returnsQuery(getMethodName()) }
341+
342+
override predicate returnsDocumentQuery(boolean asArray) {
343+
MethodSignatures::returnsDocumentQuery(getMethodName(), asArray)
344+
}
345+
346+
override predicate interpretsArgumentAsQuery(DataFlow::Node arg) {
347+
exists(int n |
348+
MethodSignatures::interpretsArgumentAsQuery(this.getMethodName(), n) and
349+
arg = this.getArgument(n)
289350
)
290351
}
291352
}
292353

293-
/**
294-
* A Mongoose query object as a result of a Query constructor invocation.
295-
*/
296-
class QueryFromConstructor extends DataFlow::NewNode {
297-
QueryFromConstructor() {
354+
private class NewQueryInvokeNode extends InvokeNode {
355+
NewQueryInvokeNode() {
298356
this = getAMongooseInstance().getAPropertyRead("Query").getAnInstantiation()
299357
}
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) }
300364
}
301365

302366
/**
303367
* Gets a data flow node referring to a Mongoose query object.
304368
*/
305369
private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
306370
(
307-
result instanceof QueryFromConstructor or
308-
result instanceof QueryFromModel or
371+
result.(InvokeNode).returnsQuery() or
309372
result.hasUnderlyingType("mongoose", "Query")
310373
) and
311374
t.start()
@@ -444,6 +507,152 @@ private module Mongoose {
444507
name = "within" or
445508
name = "wtimeout"
446509
}
510+
511+
/**
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.
515+
*/
516+
predicate returnsDocumentQuery(string name, boolean asArray) {
517+
asArray = false and name = "findOne"
518+
or
519+
asArray = true and name = "find"
520+
}
521+
}
522+
}
523+
524+
/**
525+
* Provides classes modeling the Mongoose Document class
526+
*/
527+
module Document {
528+
private class DocumentInvokeNode extends InvokeNode, DataFlow::MethodCallNode {
529+
DocumentInvokeNode() { 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+
545+
/**
546+
* A Mongoose Document that is retrieved from the backing database.
547+
*/
548+
class RetrievedDocument extends DataFlow::SourceNode {
549+
RetrievedDocument() {
550+
exists(boolean asArray, DataFlow::ParameterNode param |
551+
exists(InvokeNode call |
552+
call.returnsDocumentQuery(asArray) and
553+
param = call.getCallback(call.getNumArgument() - 1).getParameter(1)
554+
)
555+
or
556+
exists(
557+
DataFlow::SourceNode base, DataFlow::MethodCallNode call, string executor,
558+
int paramIndex
559+
|
560+
executor = "then" and paramIndex = 0
561+
or
562+
executor = "exec" and paramIndex = 1
563+
|
564+
base = Query::ref() and
565+
call = base.getAMethodCall(executor) and
566+
param = call.getCallback(0).getParameter(paramIndex) and
567+
exists(DataFlow::MethodCallNode pred |
568+
// limitation: look at the previous method call
569+
Query::MethodSignatures::returnsDocumentQuery(pred.getMethodName(), asArray) and
570+
pred.getAMethodCall() = call
571+
)
572+
)
573+
|
574+
asArray = false and this = param
575+
or
576+
asArray = true and
577+
exists(DataFlow::PropRead access |
578+
// limitation: look for direct accesses
579+
access = param.getAPropertyRead() and
580+
not exists(access.getPropertyName()) and
581+
this = access
582+
)
583+
)
584+
}
585+
}
586+
587+
/**
588+
* Gets a data flow node referring to a Mongoose Document object.
589+
*/
590+
private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
591+
(
592+
result instanceof RetrievedDocument or
593+
result.hasUnderlyingType("mongoose", "Document")
594+
) and
595+
t.start()
596+
or
597+
exists(DataFlow::TypeTracker t2, DataFlow::SourceNode succ | succ = ref(t2) |
598+
result = succ.track(t2, t)
599+
or
600+
result = succ.getAMethodCall(any(string name | MethodSignatures::returnsDocument(name))) and
601+
t = t2.continue()
602+
)
603+
}
604+
605+
/**
606+
* Gets a data flow node referring to a Mongoose Document object.
607+
*/
608+
DataFlow::SourceNode ref() { result = ref(DataFlow::TypeTracker::end()) }
609+
610+
private module MethodSignatures {
611+
/**
612+
* Holds if Document method `name` returns a Query.
613+
*/
614+
predicate returnsQuery(string name) {
615+
// Documents are subtypes of Models
616+
Model::MethodSignatures::returnsQuery(name) or
617+
name = "replaceOne" or
618+
name = "update" or
619+
name = "updateOne"
620+
}
621+
622+
/**
623+
* Holds if Document method `name` interprets parameter `n` as a query.
624+
*/
625+
predicate interpretsArgumentAsQuery(string name, int n) {
626+
// Documents are subtypes of Models
627+
Model::MethodSignatures::interpretsArgumentAsQuery(name, n)
628+
or
629+
n = 0 and
630+
(
631+
name = "replaceOne" or
632+
name = "update" or
633+
name = "updateOne"
634+
)
635+
}
636+
637+
/**
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.
641+
*/
642+
predicate returnsDocumentQuery(string name, boolean asArray) {
643+
// Documents are subtypes of Models
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"
655+
}
447656
}
448657
}
449658

@@ -468,53 +677,39 @@ private module Mongoose {
468677
* An expression that is interpreted as a (part of a) MongoDB query.
469678
*/
470679
class MongoDBQueryPart extends NoSQL::Query {
471-
MongoDBQueryPart() {
472-
exists(DataFlow::MethodCallNode mcn, string method, int n |
473-
Model::MethodSignatures::interpretsArgumentAsQuery(method, n) and
474-
mcn = Model::ref().getAMethodCall(method) and
475-
this = mcn.getArgument(n).asExpr()
476-
)
477-
or
478-
this = any(Query::QueryFromConstructor c).getArgument(2).asExpr()
479-
or
480-
exists(string method, int n | Query::MethodSignatures::interpretsArgumentAsQuery(method, n) |
481-
this = Query::ref().getAMethodCall(method).getArgument(n).asExpr()
482-
)
483-
}
680+
MongoDBQueryPart() { any(InvokeNode call).interpretsArgumentAsQuery(this.flow()) }
484681
}
485682

486683
/**
487684
* An evaluation of a MongoDB query.
488685
*/
489-
class MongoDBQueryEvaluation extends DatabaseAccess {
490-
DataFlow::MethodCallNode mcn;
686+
class ShorthandQueryEvaluation extends DatabaseAccess {
687+
InvokeNode invk;
688+
689+
ShorthandQueryEvaluation() {
690+
this = invk and
691+
// shorthand for execution: provide a callback
692+
invk.returnsQuery() and
693+
exists(invk.getCallback(invk.getNumArgument() - 1))
694+
}
491695

492-
MongoDBQueryEvaluation() {
493-
this = mcn and
494-
(
495-
exists(string method |
496-
Model::MethodSignatures::returnsQuery(method) and
497-
mcn = Model::ref().getAMethodCall(method) and
498-
// callback provided to a Model method call
499-
exists(mcn.getCallback(mcn.getNumArgument() - 1))
500-
)
501-
or
502-
Query::ref().getAMethodCall() = mcn and
503-
(
504-
// explicit execution using a Query method call
505-
exists(string executor | executor = "exec" or executor = "then" or executor = "catch" |
506-
mcn.getMethodName() = executor
507-
)
508-
or
509-
// callback provided to a Query method call
510-
exists(mcn.getCallback(mcn.getNumArgument() - 1))
511-
)
696+
override DataFlow::Node getAQueryArgument() {
697+
// NB: the complete information is not easily accessible for deeply chained calls
698+
invk.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
512707
)
513708
}
514709

515710
override DataFlow::Node getAQueryArgument() {
516-
// NB: this does not account for all of the chained calls leading to this execution
517-
mcn.getAnArgument().asExpr().(MongoDBQueryPart).flow() = result
711+
// NB: the complete information is not easily accessible for deeply chained calls
712+
none()
518713
}
519714
}
520715
}

javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@
1414
| mongoose.js:65:2:65:51 | Documen ... on(){}) |
1515
| mongoose.js:67:2:68:27 | new Mon ... on(){}) |
1616
| mongoose.js:71:2:77:9 | Documen ... .exec() |
17+
| mongoose.js:84:2:84:52 | Documen ... query)) |
18+
| mongoose.js:85:2:85:52 | Documen ... query)) |
19+
| mongoose.js:86:2:86:57 | Documen ... query)) |
20+
| mongoose.js:87:2:87:57 | Documen ... query)) |
21+
| mongoose.js:88:2:88:52 | Documen ... query)) |
22+
| mongoose.js:89:2:89:55 | Documen ... query)) |
23+
| mongoose.js:91:2:91:52 | Documen ... query)) |
24+
| mongoose.js:92:2:92:49 | Documen ... query)) |
25+
| mongoose.js:93:2:93:57 | Documen ... query)) |
26+
| mongoose.js:94:2:94:54 | Documen ... query)) |
27+
| mongoose.js:95:2:95:52 | Documen ... query)) |
28+
| mongoose.js:96:2:96:52 | Documen ... query)) |
29+
| mongoose.js:98:2:98:50 | Documen ... query)) |
1730
| socketio.js:11:5:11:54 | db.run( ... ndle}`) |
1831
| tst2.js:7:3:7:62 | sql.que ... ms.id}` |
1932
| tst2.js:9:3:9:85 | new sql ... + "'") |

0 commit comments

Comments
 (0)