Merge pull request #1907 from asger-semmle/mongoose-types

Approved by xiemaisi
This commit is contained in:
semmle-qlci
2019-09-10 12:05:57 +01:00
committed by GitHub
22 changed files with 148 additions and 22 deletions

View File

@@ -4,6 +4,8 @@
* Support for the following frameworks and libraries has been improved: * Support for the following frameworks and libraries has been improved:
- [firebase](https://www.npmjs.com/package/firebase) - [firebase](https://www.npmjs.com/package/firebase)
- [mongodb](https://www.npmjs.com/package/mongodb)
- [mongoose](https://www.npmjs.com/package/mongoose)
* The call graph has been improved to resolve method calls in more cases. This may produce more security alerts. * The call graph has been improved to resolve method calls in more cases. This may produce more security alerts.

View File

@@ -21,42 +21,91 @@ private module MongoDB {
/** /**
* Gets an access to `mongodb.MongoClient`. * Gets an access to `mongodb.MongoClient`.
*/ */
DataFlow::SourceNode getAMongoClient() { result = mongodb().getAPropertyRead("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").getArgument(1).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. * Gets an expression that may refer to a MongoDB database connection.
*/ */
DataFlow::SourceNode getAMongoDb() { private DataFlow::SourceNode getAMongoDb(DataFlow::TypeTracker t) {
result = getAMongoClient().getAMemberCall("connect").getCallback(1).getParameter(1) t.start() and
result = getAMongoDbCallback().getParameter(1)
or
exists(DataFlow::TypeTracker t2 | result = getAMongoDb(t2).track(t2, t))
} }
/** /**
* An expression that may hold a MongoDB collection. * Gets an expression that may refer to a MongoDB database connection.
*/ */
abstract class Collection extends Expr { } 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(...)`. * A collection resulting from calling `Db.collection(...)`.
*/ */
private class CollectionFromDb extends Collection { private class CollectionFromDb extends Collection {
CollectionFromDb() { CollectionFromDb() {
exists(DataFlow::CallNode collection | this = getAMongoDb().getAMethodCall("collection")
collection = getAMongoDb().getAMethodCall("collection") or
| this = getAMongoDb().getAMethodCall("collection").getCallback(1).getParameter(0)
collection.flowsToExpr(this)
or
collection.getCallback(1).getParameter(0).flowsToExpr(this)
)
} }
} }
/**
* 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. */ /** A call to a MongoDB query method. */
private class QueryCall extends DatabaseAccess, DataFlow::ValueNode { private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
override MethodCallExpr astNode;
int queryArgIdx; int queryArgIdx;
QueryCall() { QueryCall() {
exists(string m | asExpr().(MethodCallExpr).calls(any(Collection c), m) | exists(string m | this = getACollection().getAMethodCall(m) |
m = "aggregate" and queryArgIdx = 0 m = "aggregate" and queryArgIdx = 0
or or
m = "count" and queryArgIdx = 0 m = "count" and queryArgIdx = 0
@@ -91,9 +140,7 @@ private module MongoDB {
) )
} }
override DataFlow::Node getAQueryArgument() { override DataFlow::Node getAQueryArgument() { result = getArgument(queryArgIdx) }
result = DataFlow::valueNode(astNode.getArgument(queryArgIdx))
}
} }
/** /**
@@ -116,15 +163,15 @@ private module Mongoose {
/** /**
* Gets a call to `mongoose.createConnection`. * Gets a call to `mongoose.createConnection`.
*/ */
MethodCallExpr createConnection() { DataFlow::CallNode createConnection() {
result = getAMongooseInstance().getAMemberCall("createConnection").asExpr() result = getAMongooseInstance().getAMemberCall("createConnection")
} }
/** /**
* A Mongoose collection object. * A Mongoose collection object.
*/ */
class Model extends MongoDB::Collection { class Model extends MongoDB::Collection {
Model() { getAMongooseInstance().getAMemberCall("model").flowsToExpr(this) } Model() { this = getAMongooseInstance().getAMemberCall("model") }
} }
/** /**
@@ -134,7 +181,7 @@ private module Mongoose {
string kind; string kind;
Credentials() { Credentials() {
exists(string prop | createConnection().hasOptionArgument(3, prop, this) | exists(string prop | this = createConnection().getOptionArgument(3, prop).asExpr() |
prop = "user" and kind = "user name" prop = "user" and kind = "user name"
or or
prop = "pass" and kind = "password" prop = "pass" and kind = "password"

View File

@@ -0,0 +1,15 @@
nodes
| typedClient.ts:13:7:13:32 | v |
| typedClient.ts:13:11:13:32 | JSON.pa ... body.x) |
| typedClient.ts:13:22:13:29 | req.body |
| typedClient.ts:13:22:13:31 | req.body.x |
| typedClient.ts:14:24:14:32 | { id: v } |
| typedClient.ts:14:30:14:30 | v |
edges
| typedClient.ts:13:7:13:32 | v | typedClient.ts:14:30:14:30 | v |
| typedClient.ts:13:11:13:32 | JSON.pa ... body.x) | typedClient.ts:13:7:13:32 | v |
| typedClient.ts:13:22:13:29 | req.body | typedClient.ts:13:22:13:31 | req.body.x |
| typedClient.ts:13:22:13:31 | req.body.x | typedClient.ts:13:11:13:32 | JSON.pa ... body.x) |
| typedClient.ts:14:30:14:30 | v | typedClient.ts:14:24:14:32 | { id: v } |
#select
| typedClient.ts:14:24:14:32 | { id: v } | typedClient.ts:13:22:13:29 | req.body | typedClient.ts:14:24:14:32 | { id: v } | This query depends on $@. | typedClient.ts:13:22:13:29 | req.body | a user-provided value |

View File

@@ -0,0 +1,5 @@
declare module "mongodb" {
interface Collection {
find(query: any): any;
}
}

View File

@@ -0,0 +1,6 @@
{
"include": ["."],
"compilerOptions": {
"esModuleInterop": true
}
}

View File

@@ -0,0 +1,15 @@
import * as mongodb from "mongodb";
const express = require('express') as any;
const bodyParser = require('body-parser') as any;
declare function getCollection(): mongodb.Collection;
let app = express();
app.use(bodyParser.json());
app.post('/find', (req, res) => {
let v = JSON.parse(req.body.x);
getCollection().find({ id: v }); // NOT OK
});

View File

@@ -41,6 +41,15 @@ nodes
| mongooseJsonParse.js:20:19:20:50 | JSON.pa ... ).title | | mongooseJsonParse.js:20:19:20:50 | JSON.pa ... ).title |
| mongooseJsonParse.js:20:30:20:43 | req.query.data | | mongooseJsonParse.js:20:30:20:43 | req.query.data |
| mongooseJsonParse.js:23:19:23:23 | query | | mongooseJsonParse.js:23:19:23:23 | query |
| mongooseModelClient.js:10:7:10:32 | v |
| mongooseModelClient.js:10:11:10:32 | JSON.pa ... body.x) |
| mongooseModelClient.js:10:22:10:29 | req.body |
| mongooseModelClient.js:10:22:10:31 | req.body.x |
| mongooseModelClient.js:11:16:11:24 | { id: v } |
| mongooseModelClient.js:11:22:11:22 | v |
| mongooseModelClient.js:12:16:12:34 | { id: req.body.id } |
| mongooseModelClient.js:12:22:12:29 | req.body |
| mongooseModelClient.js:12:22:12:32 | req.body.id |
| socketio.js:10:25:10:30 | handle | | socketio.js:10:25:10:30 | handle |
| socketio.js:11:12:11:53 | `INSERT ... andle}` | | socketio.js:11:12:11:53 | `INSERT ... andle}` |
| socketio.js:11:46:11:51 | handle | | socketio.js:11:46:11:51 | handle |
@@ -124,6 +133,13 @@ edges
| mongooseJsonParse.js:20:19:20:50 | JSON.pa ... ).title | mongooseJsonParse.js:19:19:19:20 | {} | | mongooseJsonParse.js:20:19:20:50 | JSON.pa ... ).title | mongooseJsonParse.js:19:19:19:20 | {} |
| mongooseJsonParse.js:20:19:20:50 | JSON.pa ... ).title | mongooseJsonParse.js:23:19:23:23 | query | | mongooseJsonParse.js:20:19:20:50 | JSON.pa ... ).title | mongooseJsonParse.js:23:19:23:23 | query |
| mongooseJsonParse.js:20:30:20:43 | req.query.data | mongooseJsonParse.js:20:19:20:44 | JSON.pa ... y.data) | | mongooseJsonParse.js:20:30:20:43 | req.query.data | mongooseJsonParse.js:20:19:20:44 | JSON.pa ... y.data) |
| mongooseModelClient.js:10:7:10:32 | v | mongooseModelClient.js:11:22:11:22 | v |
| mongooseModelClient.js:10:11:10:32 | JSON.pa ... body.x) | mongooseModelClient.js:10:7:10:32 | v |
| mongooseModelClient.js:10:22:10:29 | req.body | mongooseModelClient.js:10:22:10:31 | req.body.x |
| mongooseModelClient.js:10:22:10:31 | req.body.x | mongooseModelClient.js:10:11:10:32 | JSON.pa ... body.x) |
| mongooseModelClient.js:11:22:11:22 | v | mongooseModelClient.js:11:16:11:24 | { id: v } |
| mongooseModelClient.js:12:22:12:29 | req.body | mongooseModelClient.js:12:22:12:32 | req.body.id |
| mongooseModelClient.js:12:22:12:32 | req.body.id | mongooseModelClient.js:12:16:12:34 | { id: req.body.id } |
| socketio.js:10:25:10:30 | handle | socketio.js:11:46:11:51 | handle | | socketio.js:10:25:10:30 | handle | socketio.js:11:46:11:51 | handle |
| socketio.js:11:46:11:51 | handle | socketio.js:11:12:11:53 | `INSERT ... andle}` | | socketio.js:11:46:11:51 | handle | socketio.js:11:12:11:53 | `INSERT ... andle}` |
| tst2.js:9:27:9:78 | "select ... rams.id | tst2.js:9:27:9:84 | "select ... d + "'" | | tst2.js:9:27:9:78 | "select ... rams.id | tst2.js:9:27:9:84 | "select ... d + "'" |
@@ -159,6 +175,8 @@ edges
| mongoose.js:60:25:60:29 | query | mongoose.js:21:19:21:26 | req.body | mongoose.js:60:25:60:29 | query | This query depends on $@. | mongoose.js:21:19:21:26 | req.body | a user-provided value | | mongoose.js:60:25:60:29 | query | mongoose.js:21:19:21:26 | req.body | mongoose.js:60:25:60:29 | query | This query depends on $@. | mongoose.js:21:19:21:26 | req.body | a user-provided value |
| mongoose.js:63:24:63:28 | query | mongoose.js:21:19:21:26 | req.body | mongoose.js:63:24:63:28 | query | This query depends on $@. | mongoose.js:21:19:21:26 | req.body | a user-provided value | | mongoose.js:63:24:63:28 | query | mongoose.js:21:19:21:26 | req.body | mongoose.js:63:24:63:28 | query | This query depends on $@. | mongoose.js:21:19:21:26 | req.body | a user-provided value |
| mongooseJsonParse.js:23:19:23:23 | query | mongooseJsonParse.js:20:30:20:43 | req.query.data | mongooseJsonParse.js:23:19:23:23 | query | This query depends on $@. | mongooseJsonParse.js:20:30:20:43 | req.query.data | a user-provided value | | mongooseJsonParse.js:23:19:23:23 | query | mongooseJsonParse.js:20:30:20:43 | req.query.data | mongooseJsonParse.js:23:19:23:23 | query | This query depends on $@. | mongooseJsonParse.js:20:30:20:43 | req.query.data | a user-provided value |
| mongooseModelClient.js:11:16:11:24 | { id: v } | mongooseModelClient.js:10:22:10:29 | req.body | mongooseModelClient.js:11:16:11:24 | { id: v } | This query depends on $@. | mongooseModelClient.js:10:22:10:29 | req.body | a user-provided value |
| mongooseModelClient.js:12:16:12:34 | { id: req.body.id } | mongooseModelClient.js:12:22:12:29 | req.body | mongooseModelClient.js:12:16:12:34 | { id: req.body.id } | This query depends on $@. | mongooseModelClient.js:12:22:12:29 | req.body | a user-provided value |
| socketio.js:11:12:11:53 | `INSERT ... andle}` | socketio.js:10:25:10:30 | handle | socketio.js:11:12:11:53 | `INSERT ... andle}` | This query depends on $@. | socketio.js:10:25:10:30 | handle | a user-provided value | | socketio.js:11:12:11:53 | `INSERT ... andle}` | socketio.js:10:25:10:30 | handle | socketio.js:11:12:11:53 | `INSERT ... andle}` | This query depends on $@. | socketio.js:10:25:10:30 | handle | a user-provided value |
| tst2.js:9:27:9:84 | "select ... d + "'" | tst2.js:9:66:9:78 | req.params.id | tst2.js:9:27:9:84 | "select ... d + "'" | This query depends on $@. | tst2.js:9:66:9:78 | req.params.id | a user-provided value | | tst2.js:9:27:9:84 | "select ... d + "'" | tst2.js:9:66:9:78 | req.params.id | tst2.js:9:27:9:84 | "select ... d + "'" | This query depends on $@. | tst2.js:9:66:9:78 | req.params.id | a user-provided value |
| tst3.js:10:14:10:19 | query1 | tst3.js:9:16:9:34 | req.params.category | tst3.js:10:14:10:19 | query1 | This query depends on $@. | tst3.js:9:16:9:34 | req.params.category | a user-provided value | | tst3.js:10:14:10:19 | query1 | tst3.js:9:16:9:34 | req.params.category | tst3.js:10:14:10:19 | query1 | This query depends on $@. | tst3.js:9:16:9:34 | req.params.category | a user-provided value |

View File

@@ -0,0 +1 @@
Security/CWE-089/SqlInjection.ql

View File

@@ -0,0 +1,3 @@
import mongoose from 'mongoose';
export const MyModel = mongoose.model('MyModel', getSchema());

View File

@@ -0,0 +1,14 @@
import { MyModel } from './mongooseModel';
import express from 'express';
import bodyParser from 'body-parser';
let app = express();
app.use(bodyParser.json());
app.post('/find', (req, res) => {
let v = JSON.parse(req.body.x);
MyModel.find({ id: v }); // NOT OK
MyModel.find({ id: req.body.id }); // NOT OK
MyModel.find({ id: `${req.body.id}` }); // OK
});