mirror of
https://github.com/github/codeql.git
synced 2026-05-23 23:57:06 +02:00
545 lines
16 KiB
Plaintext
545 lines
16 KiB
Plaintext
/**
|
|
* Provides classes for working with NoSQL libraries.
|
|
*/
|
|
|
|
import javascript
|
|
|
|
/** Provides classes for modeling NoSQL query sinks. */
|
|
module NoSql {
|
|
/** An expression that is interpreted as a NoSQL query. */
|
|
abstract class Query extends Expr {
|
|
/** Gets an expression that is interpreted as a code operator in this query. */
|
|
DataFlow::Node getACodeOperator() { none() }
|
|
}
|
|
}
|
|
|
|
/** DEPRECATED: Alias for NoSql */
|
|
deprecated module NoSQL = NoSql;
|
|
|
|
/**
|
|
* Gets the value of a `$where` property of an object that flows to `n`.
|
|
*/
|
|
private DataFlow::Node getADollarWherePropertyValue(DataFlow::Node n) {
|
|
result = n.getALocalSource().getAPropertyWrite("$where").getRhs()
|
|
}
|
|
|
|
/**
|
|
* Provides classes modeling the MongoDB library.
|
|
*/
|
|
private module MongoDB {
|
|
/**
|
|
* Gets an import of MongoDB.
|
|
*/
|
|
DataFlow::ModuleImportNode mongodb() { result.getPath() = "mongodb" }
|
|
|
|
/**
|
|
* Gets an access to `mongodb.MongoClient`.
|
|
*/
|
|
private DataFlow::SourceNode getAMongoClient(DataFlow::TypeTracker t) {
|
|
t.start() and
|
|
result = mongodb().getAPropertyRead("MongoClient")
|
|
or
|
|
exists(DataFlow::TypeTracker t2 | result = getAMongoClient(t2).track(t2, t))
|
|
}
|
|
|
|
/**
|
|
* Gets an access to `mongodb.MongoClient`.
|
|
*/
|
|
DataFlow::SourceNode getAMongoClient() { result = getAMongoClient(DataFlow::TypeTracker::end()) }
|
|
|
|
/** Gets a data flow node that leads to a `connect` callback. */
|
|
private DataFlow::SourceNode getAMongoDbCallback(DataFlow::TypeBackTracker t) {
|
|
t.start() and
|
|
result = getAMongoClient().getAMemberCall("connect").getLastArgument().getALocalSource()
|
|
or
|
|
exists(DataFlow::TypeBackTracker t2 | result = getAMongoDbCallback(t2).backtrack(t2, t))
|
|
}
|
|
|
|
/** Gets a data flow node that leads to a `connect` callback. */
|
|
private DataFlow::FunctionNode getAMongoDbCallback() {
|
|
result = getAMongoDbCallback(DataFlow::TypeBackTracker::end())
|
|
}
|
|
|
|
/**
|
|
* Gets an expression that may refer to a MongoDB database connection.
|
|
*/
|
|
private DataFlow::SourceNode getAMongoDb(DataFlow::TypeTracker t) {
|
|
t.start() and
|
|
(
|
|
exists(DataFlow::ParameterNode p |
|
|
p = result and
|
|
p = getAMongoDbCallback().getParameter(1) and
|
|
not p.getName().toLowerCase() = "client" // mongodb v3 provides a `Mongoclient` here
|
|
)
|
|
)
|
|
or
|
|
exists(DataFlow::TypeTracker t2 | result = getAMongoDb(t2).track(t2, t))
|
|
}
|
|
|
|
/**
|
|
* Gets an expression that may refer to a MongoDB database connection.
|
|
*/
|
|
DataFlow::SourceNode getAMongoDb() { result = getAMongoDb(DataFlow::TypeTracker::end()) }
|
|
|
|
/**
|
|
* A data flow node that may hold a MongoDB collection.
|
|
*/
|
|
abstract class Collection extends DataFlow::SourceNode { }
|
|
|
|
/**
|
|
* A collection resulting from calling `Db.collection(...)`.
|
|
*/
|
|
private class CollectionFromDb extends Collection {
|
|
CollectionFromDb() {
|
|
this = getAMongoDb().getAMethodCall("collection")
|
|
or
|
|
this = getAMongoDb().getAMethodCall("collection").getCallback(1).getParameter(0)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A collection based on the type `mongodb.Collection`.
|
|
*
|
|
* Note that this also covers `mongoose` models since they are subtypes
|
|
* of `mongodb.Collection`.
|
|
*/
|
|
private class CollectionFromType extends Collection {
|
|
CollectionFromType() { hasUnderlyingType("mongodb", "Collection") }
|
|
}
|
|
|
|
/** Gets a data flow node referring to a MongoDB collection. */
|
|
private DataFlow::SourceNode getACollection(DataFlow::TypeTracker t) {
|
|
t.start() and
|
|
result instanceof Collection
|
|
or
|
|
exists(DataFlow::TypeTracker t2 | result = getACollection(t2).track(t2, t))
|
|
}
|
|
|
|
/** Gets a data flow node referring to a MongoDB collection. */
|
|
DataFlow::SourceNode getACollection() { result = getACollection(DataFlow::TypeTracker::end()) }
|
|
|
|
/** A call to a MongoDB query method. */
|
|
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
|
|
int queryArgIdx;
|
|
|
|
QueryCall() {
|
|
exists(string m | this = getACollection().getAMethodCall(m) |
|
|
CollectionMethodSignatures::interpretsArgumentAsQuery(m, queryArgIdx)
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getAQueryArgument() { result = getArgument(queryArgIdx) }
|
|
}
|
|
|
|
/**
|
|
* Provides signatures for the Collection methods.
|
|
*/
|
|
module CollectionMethodSignatures {
|
|
/**
|
|
* Holds if Collection method `name` interprets parameter `n` as a query.
|
|
*/
|
|
predicate interpretsArgumentAsQuery(string name, int n) {
|
|
// FilterQuery
|
|
(
|
|
name = "aggregate" and n = 0
|
|
or
|
|
name = "count" and n = 0
|
|
or
|
|
name = "countDocuments" and n = 0
|
|
or
|
|
name = "deleteMany" and n = 0
|
|
or
|
|
name = "deleteOne" and n = 0
|
|
or
|
|
name = "distinct" and n = 1
|
|
or
|
|
name = "find" and n = 0
|
|
or
|
|
name = "findOne" and n = 0
|
|
or
|
|
name = "findOneAndDelete" and n = 0
|
|
or
|
|
name = "findOneAndRemove" and n = 0
|
|
or
|
|
name = "findOneAndReplace" and n = 0
|
|
or
|
|
name = "findOneAndUpdate" and n = 0
|
|
or
|
|
name = "remove" and n = 0
|
|
or
|
|
name = "replaceOne" and n = 0
|
|
or
|
|
name = "update" and n = 0
|
|
or
|
|
name = "updateMany" and n = 0
|
|
or
|
|
name = "updateOne" and n = 0
|
|
)
|
|
or
|
|
// UpdateQuery
|
|
(
|
|
name = "findOneAndUpdate" and n = 1
|
|
or
|
|
name = "update" and n = 1
|
|
or
|
|
name = "updateMany" and n = 1
|
|
or
|
|
name = "updateOne" and n = 1
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An expression that is interpreted as a MongoDB query.
|
|
*/
|
|
class Query extends NoSQL::Query {
|
|
Query() { this = any(QueryCall qc).getAQueryArgument().asExpr() }
|
|
|
|
override DataFlow::Node getACodeOperator() {
|
|
result = getADollarWherePropertyValue(this.flow())
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides classes modeling the Mongoose library.
|
|
*/
|
|
private module Mongoose {
|
|
/**
|
|
* Gets an import of Mongoose.
|
|
*/
|
|
DataFlow::ModuleImportNode getAMongooseInstance() { result.getPath() = "mongoose" }
|
|
|
|
/**
|
|
* Gets a call to `mongoose.createConnection`.
|
|
*/
|
|
DataFlow::CallNode createConnection() {
|
|
result = getAMongooseInstance().getAMemberCall("createConnection")
|
|
}
|
|
|
|
/**
|
|
* A Mongoose function invocation.
|
|
*/
|
|
private class InvokeNode extends DataFlow::InvokeNode {
|
|
/**
|
|
* Holds if this invocation returns an object of type `Query`.
|
|
*/
|
|
abstract predicate returnsQuery();
|
|
|
|
/**
|
|
* Holds if this invocation returns a `Query` that evaluates to one or
|
|
* more Documents (`asArray` is false if it evaluates to a single
|
|
* Document).
|
|
*/
|
|
abstract predicate returnsDocumentQuery(boolean asArray);
|
|
|
|
/**
|
|
* Holds if this invocation interprets `arg` as a query.
|
|
*/
|
|
abstract predicate interpretsArgumentAsQuery(DataFlow::Node arg);
|
|
}
|
|
|
|
/**
|
|
* Provides classes modeling the Mongoose Model class
|
|
*/
|
|
module Model {
|
|
private class ModelInvokeNode extends InvokeNode, DataFlow::MethodCallNode {
|
|
ModelInvokeNode() { this = ref().getAMethodCall() }
|
|
|
|
override predicate returnsQuery() { MethodSignatures::returnsQuery(getMethodName()) }
|
|
|
|
override predicate returnsDocumentQuery(boolean asArray) {
|
|
MethodSignatures::returnsDocumentQuery(getMethodName(), asArray)
|
|
}
|
|
|
|
override predicate interpretsArgumentAsQuery(DataFlow::Node arg) {
|
|
exists(int n |
|
|
MethodSignatures::interpretsArgumentAsQuery(this.getMethodName(), n) and
|
|
arg = this.getArgument(n)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a data flow node referring to a Mongoose Model object.
|
|
*/
|
|
private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
|
|
(
|
|
result = getAMongooseInstance().getAMemberCall("model")
|
|
or
|
|
exists(DataFlow::SourceNode conn | conn = createConnection() |
|
|
result = conn.getAMemberCall("model") or
|
|
result = conn.getAPropertyRead("models").getAPropertyRead()
|
|
)
|
|
or
|
|
result.hasUnderlyingType("mongoose", "Model")
|
|
) and
|
|
t.start()
|
|
or
|
|
exists(DataFlow::TypeTracker t2 | result = ref(t2).track(t2, t))
|
|
}
|
|
|
|
/**
|
|
* Gets a data flow node referring to a Mongoose model object.
|
|
*/
|
|
DataFlow::SourceNode ref() { result = ref(DataFlow::TypeTracker::end()) }
|
|
|
|
/**
|
|
* Provides signatures for the Model methods.
|
|
*/
|
|
module MethodSignatures {
|
|
/**
|
|
* Holds if Model method `name` interprets parameter `n` as a query.
|
|
*/
|
|
predicate interpretsArgumentAsQuery(string name, int n) {
|
|
// implement lots of the MongoDB collection interface
|
|
MongoDB::CollectionMethodSignatures::interpretsArgumentAsQuery(name, n)
|
|
or
|
|
name = "findByIdAndUpdate" and n = 1
|
|
or
|
|
name = "where" and n = 0
|
|
}
|
|
|
|
/**
|
|
* Holds if Model method `name` returns a Query.
|
|
*/
|
|
predicate returnsQuery(string name) {
|
|
name = "$where" or
|
|
name = "count" or
|
|
name = "countDocuments" or
|
|
name = "deleteMany" or
|
|
name = "deleteOne" or
|
|
name = "find" or
|
|
name = "findById" or
|
|
name = "findByIdAndDelete" or
|
|
name = "findByIdAndRemove" or
|
|
name = "findByIdAndUpdate" or
|
|
name = "findOne" or
|
|
name = "findOneAndDelete" or
|
|
name = "findOneAndRemove" or
|
|
name = "findOneAndReplace" or
|
|
name = "findOneAndUpdate" or
|
|
name = "geosearch" or
|
|
name = "replaceOne" or
|
|
name = "update" or
|
|
name = "updateMany" or
|
|
name = "updateOne" or
|
|
name = "where"
|
|
}
|
|
|
|
/**
|
|
* Holds if Document method `name` returns a query that results in
|
|
* one or more documents, the documents are wrapped in an array
|
|
* if `asArray` is true.
|
|
*/
|
|
predicate returnsDocumentQuery(string name, boolean asArray) {
|
|
asArray = false and name = "findOne"
|
|
or
|
|
asArray = true and name = "find"
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides classes modeling the Mongoose Document class
|
|
*/
|
|
module Document {
|
|
private class DocumentInvokeNode extends InvokeNode, DataFlow::MethodCallNode {
|
|
DocumentInvokeNode() { this = ref().getAMethodCall() }
|
|
|
|
override predicate returnsQuery() { MethodSignatures::returnsQuery(getMethodName()) }
|
|
|
|
override predicate returnsDocumentQuery(boolean asArray) {
|
|
MethodSignatures::returnsDocumentQuery(getMethodName(), asArray)
|
|
}
|
|
|
|
override predicate interpretsArgumentAsQuery(DataFlow::Node arg) {
|
|
exists(int n |
|
|
MethodSignatures::interpretsArgumentAsQuery(this.getMethodName(), n) and
|
|
arg = this.getArgument(n)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A Mongoose Document that is retrieved from the backing database.
|
|
*/
|
|
class RetrievedDocument extends DataFlow::SourceNode {
|
|
RetrievedDocument() {
|
|
exists(boolean asArray, DataFlow::ParameterNode param |
|
|
exists(InvokeNode call |
|
|
call.returnsDocumentQuery(asArray) and
|
|
param = call.getCallback(call.getNumArgument() - 1).getParameter(1)
|
|
)
|
|
|
|
|
asArray = false and this = param
|
|
or
|
|
asArray = true and
|
|
exists(DataFlow::PropRead access |
|
|
// limitation: look for direct accesses
|
|
access = param.getAPropertyRead() and
|
|
not exists(access.getPropertyName()) and
|
|
this = access
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a data flow node referring to a Mongoose Document object.
|
|
*/
|
|
private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
|
|
(
|
|
result instanceof RetrievedDocument or
|
|
result.hasUnderlyingType("mongoose", "Document")
|
|
) and
|
|
t.start()
|
|
or
|
|
exists(DataFlow::TypeTracker t2, DataFlow::SourceNode succ | succ = ref(t2) |
|
|
result = succ.track(t2, t)
|
|
or
|
|
result = succ.getAMethodCall(any(string name | MethodSignatures::returnsDocument(name))) and
|
|
t = t2.continue()
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets a data flow node referring to a Mongoose Document object.
|
|
*/
|
|
DataFlow::SourceNode ref() { result = ref(DataFlow::TypeTracker::end()) }
|
|
|
|
private module MethodSignatures {
|
|
/**
|
|
* Holds if Document method `name` returns a Query.
|
|
*/
|
|
predicate returnsQuery(string name) {
|
|
// Documents are subtypes of Models
|
|
Model::MethodSignatures::returnsQuery(name) or
|
|
name = "replaceOne" or
|
|
name = "update" or
|
|
name = "updateOne"
|
|
}
|
|
|
|
/**
|
|
* Holds if Document method `name` interprets parameter `n` as a query.
|
|
*/
|
|
predicate interpretsArgumentAsQuery(string name, int n) {
|
|
// Documents are subtypes of Models
|
|
Model::MethodSignatures::interpretsArgumentAsQuery(name, n)
|
|
or
|
|
n = 0 and
|
|
(
|
|
name = "replaceOne" or
|
|
name = "update" or
|
|
name = "updateOne"
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Holds if Document method `name` returns a query that results in
|
|
* one or more documents, the documents are wrapped in an array
|
|
* if `asArray` is true.
|
|
*/
|
|
predicate returnsDocumentQuery(string name, boolean asArray) {
|
|
// Documents are subtypes of Models
|
|
Model::MethodSignatures::returnsDocumentQuery(name, asArray)
|
|
}
|
|
|
|
/**
|
|
* Holds if Document method `name` returns a Document.
|
|
*/
|
|
predicate returnsDocument(string name) {
|
|
name = ["depopulate", "init", "populate", "overwrite"]
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An expression passed to `mongoose.createConnection` to supply credentials.
|
|
*/
|
|
class Credentials extends CredentialsExpr {
|
|
string kind;
|
|
|
|
Credentials() {
|
|
exists(string prop | this = createConnection().getOptionArgument(3, prop).asExpr() |
|
|
prop = "user" and kind = "user name"
|
|
or
|
|
prop = "pass" and kind = "password"
|
|
)
|
|
}
|
|
|
|
override string getCredentialsKind() { result = kind }
|
|
}
|
|
|
|
/**
|
|
* An expression that is interpreted as a (part of a) MongoDB query.
|
|
*/
|
|
class MongoDBQueryPart extends NoSql::Query {
|
|
MongoDBQueryPart() { any(InvokeNode call).interpretsArgumentAsQuery(this.flow()) }
|
|
|
|
override DataFlow::Node getACodeOperator() {
|
|
result = getADollarWherePropertyValue(this.flow())
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An evaluation of a MongoDB query.
|
|
*/
|
|
class ShorthandQueryEvaluation extends DatabaseAccess {
|
|
InvokeNode invk;
|
|
|
|
ShorthandQueryEvaluation() {
|
|
this = invk and
|
|
// shorthand for execution: provide a callback
|
|
invk.returnsQuery() and
|
|
exists(invk.getCallback(invk.getNumArgument() - 1))
|
|
}
|
|
|
|
override DataFlow::Node getAQueryArgument() {
|
|
// NB: the complete information is not easily accessible for deeply chained calls
|
|
invk.interpretsArgumentAsQuery(result)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An access to a database through redis
|
|
*/
|
|
class RedisDatabaseAccess extends DatabaseAccess, DataFlow::CallNode {
|
|
RedisDatabaseAccess() { this = redis().getMember(_).getACall() }
|
|
|
|
override DataFlow::Node getAResult() {
|
|
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
|
|
}
|
|
|
|
override DataFlow::Node getAQueryArgument() { none() }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides classes modeling the `ioredis` library.
|
|
*
|
|
* ```
|
|
* import Redis from 'ioredis'
|
|
* let client = new Redis(...)
|
|
* ```
|
|
*/
|
|
private module IoRedis {
|
|
/**
|
|
* Gets an `ioredis` client.
|
|
*/
|
|
API::Node ioredis() { result = API::moduleImport("ioredis").getInstance() }
|
|
|
|
/**
|
|
* An access to a database through ioredis
|
|
*/
|
|
class IoRedisDatabaseAccess extends DatabaseAccess, DataFlow::CallNode {
|
|
IoRedisDatabaseAccess() { this = ioredis().getMember(_).getACall() }
|
|
|
|
override DataFlow::Node getAResult() {
|
|
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
|
|
}
|
|
|
|
override DataFlow::Node getAQueryArgument() { none() }
|
|
}
|
|
}
|