add database accesses as additional (heuristic) remote flow sources

This commit is contained in:
Stephan Brandauer
2021-12-08 14:30:33 +01:00
parent 2a36744deb
commit 132e0bf4b7
9 changed files with 547 additions and 22 deletions

View File

@@ -84,6 +84,21 @@ abstract class FileNameSource extends DataFlow::Node { }
abstract class DatabaseAccess extends DataFlow::Node {
/** Gets an argument to this database access that is interpreted as a query. */
abstract DataFlow::Node getAQueryArgument();
/** Gets a node to which a result of the access may flow. */
DataFlow::Node getAResult() {
none() // Overridden in subclass
}
/**
* Holds if the data returned can be a user-controlled object,
* such as a JSON object parsed from user-controlled data.
*/
predicate returnsUserControlledObject() {
// NB: Most data bases support JSON data (some via plugins),
// which is why this has a default implementation.
any()
}
}
/**

View File

@@ -46,17 +46,35 @@ module Knex {
RawKnexSqlString() { this = any(RawKnexCall call).getArgument(0).asExpr() }
}
/** A call that triggers a SQL query submission. */
private class KnexDatabaseAccess extends DatabaseAccess {
KnexDatabaseAccess() {
this = knexObject().getMember(["then", "stream", "asCallback"]).getACall()
/** A call that triggers a SQL query submission by calling then/stream/asCallback. */
private class KnexDatabaseCallback extends DatabaseAccess, DataFlow::CallNode {
string member;
KnexDatabaseCallback() {
member = ["then", "stream", "asCallback"] and
this = knexObject().getMember(member).getACall()
}
override DataFlow::Node getAResult() {
member = "then" and
result = this.getCallback(0).getParameter(0)
or
exists(AwaitExpr await |
this = await.flow() and
await.getOperand() = knexObject().getAUse().asExpr()
)
member = "asCallback" and
result = this.getCallback(0).getParameter(1)
}
override DataFlow::Node getAQueryArgument() { none() }
}
private class KnexDatabaseAwait extends DatabaseAccess, DataFlow::ValueNode {
KnexDatabaseAwait() {
exists(AwaitExpr enclosingAwait | this = enclosingAwait.flow() |
enclosingAwait.getOperand() = knexObject().getAUse().asExpr()
)
}
override DataFlow::Node getAResult() { result = this }
override DataFlow::Node getAQueryArgument() { none() }
}
}

View File

@@ -3,6 +3,7 @@
*/
import javascript
import semmle.javascript.Promises
module NoSQL {
/** An expression that is interpreted as a NoSQL query. */
@@ -65,6 +66,10 @@ private module MongoDB {
override DataFlow::Node getAQueryArgument() { result = this.getArgument(queryArgIdx) }
override DataFlow::Node getAResult() {
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
}
DataFlow::Node getACodeOperator() {
result = getADollarWhereProperty(this.getParameter(queryArgIdx))
}
@@ -537,12 +542,29 @@ private module Mongoose {
// NB: the complete information is not easily accessible for deeply chained calls
f.getQueryArgument().getARhs() = result
}
override DataFlow::Node getAResult() {
result = this.getCallback(this.getNumArgument() - 1).getParameter(1)
}
}
class ExplicitQueryEvaluation extends DatabaseAccess {
class ExplicitQueryEvaluation extends DatabaseAccess, DataFlow::CallNode {
string member;
ExplicitQueryEvaluation() {
// explicit execution using a Query method call
Query::getAMongooseQuery().getMember(["exec", "then", "catch"]).getACall() = this
member = ["exec", "then", "catch"] and
Query::getAMongooseQuery().getMember(member).getACall() = this
}
private int resultParamIndex() {
member = "then" and result = 0
or
member = "exec" and result = 1
}
override DataFlow::Node getAResult() {
result = this.getCallback(_).getParameter(this.resultParamIndex())
}
override DataFlow::Node getAQueryArgument() {
@@ -588,6 +610,10 @@ private module Minimongo {
override DataFlow::Node getAQueryArgument() { result = this.getArgument(queryArgIdx) }
override DataFlow::Node getAResult() {
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
}
DataFlow::Node getACodeOperator() {
result = getADollarWhereProperty(this.getParameter(queryArgIdx))
}
@@ -609,7 +635,7 @@ private module Minimongo {
* Provides classes modeling the MarsDB library.
*/
private module MarsDB {
private class MarsDBAccess extends DatabaseAccess {
private class MarsDBAccess extends DatabaseAccess, DataFlow::CallNode {
string method;
MarsDBAccess() {
@@ -623,21 +649,29 @@ private module MarsDB {
string getMethod() { result = method }
override DataFlow::Node getAResult() {
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
}
override DataFlow::Node getAQueryArgument() { none() }
}
/** A call to a MarsDB query method. */
private class QueryCall extends DatabaseAccess, API::CallNode {
private class QueryCall extends MarsDBAccess, API::CallNode {
int queryArgIdx;
QueryCall() {
exists(string m |
this.(MarsDBAccess).getMethod() = m and
this.getMethod() = m and
// implements parts of the Minimongo interface
Minimongo::CollectionMethodSignatures::interpretsArgumentAsQuery(m, queryArgIdx)
)
}
override DataFlow::Node getAResult() {
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
}
override DataFlow::Node getAQueryArgument() { result = this.getArgument(queryArgIdx) }
DataFlow::Node getACodeOperator() {
@@ -744,9 +778,13 @@ private module Redis {
/**
* An access to a database through redis
*/
class RedisDatabaseAccess extends DatabaseAccess {
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() }
}
}
@@ -768,9 +806,13 @@ private module IoRedis {
/**
* An access to a database through ioredis
*/
class IoRedisDatabaseAccess extends DatabaseAccess {
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() }
}
}

View File

@@ -3,6 +3,7 @@
*/
import javascript
import semmle.javascript.Promises
module SQL {
/** A string-valued expression that is interpreted as a SQL command. */
@@ -81,6 +82,8 @@ private module MySql {
)
}
override DataFlow::Node getAResult() { result = this.getCallback(_).getParameter(1) }
override DataFlow::Node getAQueryArgument() { result = this.getArgument(0) }
}
@@ -178,6 +181,16 @@ private module Postgres {
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() { this = [client(), pool()].getMember("query").getACall() }
override DataFlow::Node getAResult() {
this.getNumArgument() = 2 and
result = this.getCallback(1).getParameter(1)
or
this.getNumArgument() = 1 and
result = this.getAMethodCall("then").getCallback(0).getParameter(0)
or
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
}
override DataFlow::Node getAQueryArgument() { result = this.getArgument(0) }
}
@@ -322,6 +335,10 @@ private module Postgres {
)
}
override DataFlow::Node getAResult() {
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
}
override DataFlow::Node getAQueryArgument() {
result = this.getADirectQueryArgument()
or
@@ -370,6 +387,11 @@ private module Sqlite {
this = database().getMember("prepare").getACall()
}
override DataFlow::Node getAResult() {
result = this.getCallback(1).getParameter(1) or
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
}
override DataFlow::Node getAQueryArgument() { result = this.getArgument(0) }
}
@@ -420,6 +442,11 @@ private module MsSql {
mssql().getMember("query").getAUse() = DataFlow::valueNode(astNode.getTag())
}
override DataFlow::Node getAResult() {
PromiseFlow::loadStep(this.(DataFlow::SourceNode).getALocalUse(), result,
Promises::valueProp())
}
override DataFlow::Node getAQueryArgument() {
result = DataFlow::valueNode(astNode.getTemplate().getAnElement())
}
@@ -429,6 +456,12 @@ private module MsSql {
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() { this = [mssql(), request()].getMember(["query", "batch"]).getACall() }
override DataFlow::Node getAResult() {
result = this.getCallback(1).getParameter(1)
or
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
}
override DataFlow::Node getAQueryArgument() { result = this.getArgument(0) }
}
@@ -491,6 +524,31 @@ private module Sequelize {
}
}
/** Gets an import of the `sequelize` module or one that re-exports it. */
API::Node sequelize() { result = API::moduleImport(["sequelize", "sequelize-typescript"]) }
/** Gets an expression that creates an instance of the `Sequelize` class. */
API::Node instance() {
result = [sequelize(), sequelize().getMember("Sequelize")].getInstance()
or
result = API::Node::ofType(["sequelize", "sequelize-typescript"], ["Sequelize", "default"])
}
/** A call to `Sequelize.query`. */
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() { this = instance().getMember("query").getACall() }
override DataFlow::Node getAResult() {
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
}
override DataFlow::Node getAQueryArgument() {
result = this.getArgument(0)
or
result = this.getOptionArgument(0, "query")
}
}
class SequelizeSink extends ModelInput::SinkModelCsv {
override predicate row(string row) {
row =
@@ -540,3 +598,145 @@ private module SpannerCsv {
}
}
}
/**
* Provides classes modeling the Google Cloud Spanner library.
*/
private module Spanner {
/**
* Gets a node that refers to the `Spanner` class
*/
API::Node spanner() {
// older versions
result = API::moduleImport("@google-cloud/spanner")
or
// newer versions
result = API::moduleImport("@google-cloud/spanner").getMember("Spanner")
}
/**
* Gets a node that refers to an instance of the `Database` class.
*/
API::Node database() {
result =
spanner().getReturn().getMember("instance").getReturn().getMember("database").getReturn()
or
result = API::Node::ofType("@google-cloud/spanner", "Database")
}
/**
* Gets a node that refers to an instance of the `v1.SpannerClient` class.
*/
API::Node v1SpannerClient() {
result = spanner().getMember("v1").getMember("SpannerClient").getInstance()
or
result = API::Node::ofType("@google-cloud/spanner", "v1.SpannerClient")
}
/**
* Gets a node that refers to a transaction object.
*/
API::Node transaction() {
result =
database()
.getMember(["runTransaction", "runTransactionAsync"])
.getParameter([0, 1])
.getParameter(1)
or
result = API::Node::ofType("@google-cloud/spanner", "Transaction")
}
/**
* Gets a node that refers to a snapshot object.
*/
API::Node snapshot() {
result = database().getMember("getSnapshot").getParameter([0, 1]).getParameter(1)
or
result = API::Node::ofType("@google-cloud/spanner", "Snapshot")
}
/** Gets an API node referring to a `BatchTransaction` object. */
API::Node batchTransaction() {
result = database().getMember("batchTransaction").getReturn()
or
result = database().getMember("createBatchTransaction").getReturn().getPromised()
or
result = API::Node::ofType("@google-cloud/spanner", "BatchTransaction")
}
/**
* A call to a Spanner method that executes a SQL query.
*/
abstract class SqlExecution extends DatabaseAccess, DataFlow::InvokeNode { }
/**
* A SQL execution that takes the input directly in the first argument or in the `sql` option.
*/
class SqlExecutionDirect extends SqlExecution {
SqlExecutionDirect() {
this = database().getMember(["run", "runPartitionedUpdate", "runStream"]).getACall()
or
this = transaction().getMember(["run", "runStream", "runUpdate"]).getACall()
or
this = batchTransaction().getMember("createQueryPartitions").getACall()
or
this = snapshot().getMember(["run", "runStream"]).getACall()
}
override DataFlow::Node getAResult() {
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
or
this = [database(), transaction(), snapshot()].getMember("run").getACall() and
result = this.getCallback(_).getParameter(1)
}
override DataFlow::Node getAQueryArgument() {
result = this.getArgument(0)
or
result = this.getOptionArgument(0, "sql")
}
}
/**
* A SQL execution that takes an array of SQL strings or { sql: string } objects.
*/
class SqlExecutionBatch extends SqlExecution, API::CallNode {
SqlExecutionBatch() { this = transaction().getMember("batchUpdate").getACall() }
override DataFlow::Node getAResult() {
none() // no results, batch update callbacks get only row counts.
}
override DataFlow::Node getAQueryArgument() {
// just use the whole array as the query argument, as arrays becomes tainted if one of the elements
// are tainted
result = this.getArgument(0)
or
result = this.getParameter(0).getUnknownMember().getMember("sql").getARhs()
}
}
/**
* A SQL execution that only takes the input in the `sql` option, and do not accept query strings
* directly.
*/
class SqlExecutionWithOption extends SqlExecution, DataFlow::CallNode {
SqlExecutionWithOption() {
this = v1SpannerClient().getMember(["executeSql", "executeStreamingSql"]).getACall()
}
override DataFlow::Node getAResult() {
this = v1SpannerClient().getMember("executeSql").getACall() and
result = this.getCallback(_).getParameter(1)
}
override DataFlow::Node getAQueryArgument() { result = this.getOptionArgument(0, "sql") }
}
/**
* An expression that is interpreted as a SQL string.
*/
class QueryString extends SQL::SqlString {
QueryString() { this = any(SqlExecution se).getAQueryArgument().asExpr() }
}
}

View File

@@ -14,7 +14,7 @@ private import semmle.javascript.security.dataflow.CommandInjectionCustomization
abstract class HeuristicSource extends DataFlow::Node { }
/**
* An access to a password, viewed a source of remote flow.
* An access to a password, viewed as a source of remote flow.
*/
private class RemoteFlowPassword extends HeuristicSource, RemoteFlowSource {
RemoteFlowPassword() { isReadFrom(this, "(?is).*(password|passwd).*") }
@@ -52,3 +52,16 @@ class RemoteServerResponse extends HeuristicSource, RemoteFlowSource {
override string getSourceType() { result = "a response from a remote server" }
}
/**
* The data read from a database.
*/
class DatabaseAccessResultRemoteFlowSource extends HeuristicSource, RemoteFlowSource {
DatabaseAccessResultRemoteFlowSource() { exists(DatabaseAccess dba | this = dba.getAResult()) }
override string getSourceType() { result = "Database query result" }
override predicate isUserControlledObject() {
this.(DatabaseAccess).returnsUserControlledObject()
}
}

View File

@@ -1,4 +1,4 @@
import javascript
private import semmle.javascript.heuristics.AdditionalSinks
select any(HeuristicSink s)
select any(HeuristicSink s | s.getFile().getBaseName() = "sinks.js")

View File

@@ -1,2 +0,0 @@
| sources.js:2:5:2:12 | password |
| sources.js:3:5:3:20 | JSON.stringify() |

View File

@@ -1,4 +1,13 @@
import javascript
private import semmle.javascript.heuristics.AdditionalSources
import testUtilities.ConsistencyChecking
select any(HeuristicSource s)
class Taint extends TaintTracking::Configuration {
Taint() { this = "Taint" }
override predicate isSource(DataFlow::Node node) { node instanceof HeuristicSource }
override predicate isSink(DataFlow::Node node) {
node = any(DataFlow::CallNode call | call.getCalleeName() = "sink").getAnArgument()
}
}

View File

@@ -1,4 +1,234 @@
const pg = require('pg');
const { Stream } = require('stream');
const pool = new pg.Pool({});
function sink(data) {}
(function() {
password;
JSON.stringify();
const password = '1234';
sink(password); // NOT OK
const s = JSON.stringify();
sink(s); // NOT OK
})();
(async function() {
const knex = require('knex');
const users = knex().select('*').from('users');
users.then(function (users) {
sink(users); // NOT OK
});
users.asCallback(function (err, users) {
sink(users); // NOT OK
});
sink(await users); // NOT OK
})();
(function() {
const pg = require('pg');
const pool = new pg.Pool({});
pool.connect(async function (err, client, done) {
client.query('SELECT * FROM users', function (err, users) {
sink(users);
});
const thenable = client.query('SELECT * FROM users')
thenable.then(function(users) {
sink(users); // NOT OK
});
const pgpromise = client.query('SELECT * FROM users');
sink(await pgpromise); // NOT OK
});
})();
(async function () {
const pgpromise = require('pg-promise')();
const db = pgpromise('postgres://username:password@localhost:1234/database');
const pgppromise = db.any('SELECT * FROM users');
pgppromise.then(function (users) {
sink(users);
});
sink(await pgppromise);
})();
(function () {
const mysql = require('mysql2');
const conn = mysql.createConnection({});
conn.query(
'SELECT * FROM `users`',
function(err, users, fields) {
sink(users); // NOT OK
}
);
conn.execute(
'SELECT * FROM `users` WHERE name = ?',
['Alice'],
function(err, users) {
sink(users);
}
);
})();
(async function () {
const sqlite = require('sqlite3');
const db = new sqlite.Database(':memory:');
db.all('SELECT * FROM users', function (err, users) {
sink(users); // NOT OK
});
const sqlitepromise = db.all('SELECT * FROM users');
sink(await sqlitepromise); // NOT OK
})();
(async function () {
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize('sqlite::memory:');
class User extends sequelize.Model {}
User.init({ name: sequelize.DataTypes.String }, { sequelize, modelName: 'user' });
sequelize.query('SELECT * FROM users').then(function (users) {
sink(users); // NOT OK
});
})();
(async function () {
const sql = require('mssql');
await sql.connect('...');
sql.query('SELECT * FROM users', function (err, users) {
sink(users); // NOT OK
});
const mssqlthenable = sql.query('SELECT * FROM users');
mssqlthenable.then(function (users) {
sink(users); // NOT OK
});
const mssqlpromise = sql.query('SELECT * FROM users');
sink(await mssqlpromise); // NOT OK
const uname = 'Alice';
const mssqltaggedquery = sql.query`SELECT * FROM users where name=${uname}`
sink(await mssqltaggedquery); // NOT OK
})();
(async function () {
const {Spanner} = require('@google-cloud/spanner');
const db = new Spanner({projectId: 'test'})
.instance('instanceid')
.database('databaseid');
const spannerpromise = db.run({
sql: 'SELECT * FROM users'
});
sink(await spannerpromise); // NOT OK
db.run({
sql: 'SELECT * FROM users'
}, function (err, rows, stats, meta) {
sink(rows); // NOT OK
});
const client = new Spanner.v1.SpannerClient({});
client.executeSql('SELECT * FROM users', {}, function (err, users) {
sink(users); // NOT OK
});
db.runTransaction(function(err, txn) {
txn.run('SELECT * FROM users', function (err, users) {
sink(users); // NOT OK
});
});
db.getSnapshot(function (err, txn) {
txn.run('SELECT * FROM users', function (err, users) {
sink(users); // NOT OK
});
txn.end();
});
})();
(function () {
const { MongoClient } = require('mongodb');
MongoClient.connect('mongodb://localhost:1234', async function (err, db) {
const collection = db.collection('users');
const users = await collection.find({});
sink(users); // NOT OK
});
})();
(async function () {
const mongoose = require('mongoose');
await mongoose.connect('mongodb://localhost:1234');
const User = mongoose.model('User', {
name: {
type: String,
unique: true
}
});
User.find({ name: 'Alice' }, function (err, alice) {
sink(alice); // NOT OK
});
User.find({ name: 'Bob' }).exec(function (err, bob) {
sink(bob); // NOT OK
});
const promise = User.find({ name: 'Claire' });
promise.then(c => sink(c)); // NOT OK
})();
(async function () {
const minimongo = require('minimongo');
const LocalDb = minimongo.MemoryDb;
const db = new LocalDb();
const doc = db.users;
const users = await doc.find({});
sink(users); // NOT OK
})();
(async function () {
const { Collection } = require('marsdb');
const doc = new Collection('users');
const users = await doc.find({});
sink(users); // NOT OK
})();
(async function () {
const redis = require("redis");
const client = redis.createClient();
const alice = await client.get('alice');
sink(alice); // NOT OK
})();
(async function () {
const Redis = require('ioredis');
const redis = new Redis();
const bob = await redis.get('bob');
sink(bob); // NOT OK
})();