Files
codeql/javascript/ql/lib/semmle/javascript/frameworks/SQL.qll
2022-05-25 08:59:53 +00:00

611 lines
20 KiB
Plaintext

/**
* Provides classes for working with SQL connectors.
*/
import javascript
module SQL {
/** A string-valued expression that is interpreted as a SQL command. */
abstract class SqlString extends Expr { }
private class SqlStringFromModel extends SqlString {
SqlStringFromModel() { this = ModelOutput::getASinkNode("sql-injection").getARhs().asExpr() }
}
/**
* An expression that sanitizes a string to make it safe to embed into
* a SQL command.
*/
abstract class SqlSanitizer extends Expr {
Expr input;
Expr output;
/** Gets the input expression being sanitized. */
Expr getInput() { result = input }
/** Gets the output expression containing the sanitized value. */
Expr getOutput() { result = output }
}
}
/**
* Provides classes modeling the (API compatible) `mysql` and `mysql2` packages.
*/
private module MySql {
private DataFlow::SourceNode mysql() { result = DataFlow::moduleImport(["mysql", "mysql2"]) }
private DataFlow::CallNode createPool() { result = mysql().getAMemberCall("createPool") }
/** Gets a reference to a MySQL pool. */
private DataFlow::SourceNode pool(DataFlow::TypeTracker t) {
t.start() and
result = createPool()
or
exists(DataFlow::TypeTracker t2 | result = pool(t2).track(t2, t))
}
/** Gets a reference to a MySQL pool. */
private DataFlow::SourceNode pool() { result = pool(DataFlow::TypeTracker::end()) }
/** Gets a call to `mysql.createConnection`. */
DataFlow::CallNode createConnection() { result = mysql().getAMemberCall("createConnection") }
/** Gets a reference to a MySQL connection instance. */
private DataFlow::SourceNode connection(DataFlow::TypeTracker t) {
t.start() and
(
result = createConnection()
or
result = pool().getAMethodCall("getConnection").getABoundCallbackParameter(0, 1)
)
or
exists(DataFlow::TypeTracker t2 | result = connection(t2).track(t2, t))
}
/** Gets a reference to a MySQL connection instance. */
DataFlow::SourceNode connection() { result = connection(DataFlow::TypeTracker::end()) }
/** A call to the MySql `query` method. */
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() { this = [pool(), connection()].getAMethodCall("query") }
override DataFlow::Node getAResult() { result = this.getCallback(_).getParameter(1) }
override DataFlow::Node getAQueryArgument() {
result = this.getArgument(0) or result = this.getOptionArgument(0, "sql")
}
}
/** An expression that is passed to the `query` method and hence interpreted as SQL. */
class QueryString extends SQL::SqlString {
QueryString() { this = any(QueryCall qc).getAQueryArgument().asExpr() }
}
/** A call to the `escape` or `escapeId` method that performs SQL sanitization. */
class EscapingSanitizer extends SQL::SqlSanitizer, MethodCallExpr {
EscapingSanitizer() {
this = [mysql(), pool(), connection()].getAMethodCall(["escape", "escapeId"]).asExpr() and
input = this.getArgument(0) and
output = this
}
}
/** An expression that is passed as user name or password to `mysql.createConnection`. */
class Credentials extends CredentialsExpr {
string kind;
Credentials() {
exists(string prop |
this = [createConnection(), createPool()].getOptionArgument(0, prop).asExpr() and
(
prop = "user" and kind = "user name"
or
prop = "password" and kind = prop
)
)
}
override string getCredentialsKind() { result = kind }
}
}
/**
* Provides classes modeling the PostgreSQL packages, such as `pg` and `pg-promise`.
*/
private module Postgres {
/** Gets an expression that constructs a new connection pool. */
DataFlow::InvokeNode newPool() {
// new require('pg').Pool()
result = DataFlow::moduleImport("pg").getAConstructorInvocation("Pool")
or
// new require('pg-pool')
result = DataFlow::moduleImport("pg-pool").getAnInstantiation()
}
/** Gets a creation of a Postgres client. */
DataFlow::InvokeNode newClient() {
result = DataFlow::moduleImport("pg").getAConstructorInvocation("Client")
}
/** A call to the Postgres `query` method. */
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() { this = [newClient(), newPool()].getAMethodCall("query") }
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) }
}
/** An expression that is passed to the `query` method and hence interpreted as SQL. */
class QueryString extends SQL::SqlString {
QueryString() {
this = any(QueryCall qc).getAQueryArgument().asExpr()
or
this = API::moduleImport("pg-cursor").getParameter(0).getARhs().asExpr()
}
}
/** An expression that is passed as user name or password when creating a client or a pool. */
class Credentials extends CredentialsExpr {
string kind;
Credentials() {
exists(string prop | this = [newClient(), newPool()].getOptionArgument(0, prop).asExpr() |
prop = "user" and kind = "user name"
or
prop = "password" and kind = prop
)
}
override string getCredentialsKind() { result = kind }
}
/** Gets a node referring to the `pg-promise` library (which is not itself a Promise). */
API::Node pgPromise() { result = API::moduleImport("pg-promise") }
/** Gets an initialized `pg-promise` library. */
API::Node pgpMain() {
result = pgPromise().getReturn()
or
result = API::Node::ofType("pg-promise", "IMain")
}
/** Gets a database from `pg-promise`. */
API::Node pgpDatabase() {
result = pgpMain().getReturn()
or
result = API::Node::ofType("pg-promise", "IDatabase")
}
/** Gets a connection created from a `pg-promise` database. */
API::Node pgpConnection() {
result = pgpDatabase().getMember("connect").getReturn().getPromised()
or
result = API::Node::ofType("pg-promise", "IConnected")
}
/** Gets a `pg-promise` task object. */
API::Node pgpTask() {
exists(API::Node taskMethod |
taskMethod = pgpObject().getMember(["task", "taskIf", "tx", "txIf"])
|
result = taskMethod.getParameter([0, 1]).getParameter(0)
or
result = taskMethod.getParameter(0).getMember("cnd").getParameter(0)
)
or
result = API::Node::ofType("pg-promise", "ITask")
}
/** Gets a `pg-promise` object which supports querying (database, connection, or task). */
API::Node pgpObject() {
result = [pgpDatabase(), pgpConnection(), pgpTask()]
or
result = API::Node::ofType("pg-promise", "IBaseProtocol")
}
private string pgpQueryMethodName() {
result =
[
"any", "each", "many", "manyOrNone", "map", "multi", "multiResult", "none", "one",
"oneOrNone", "query", "result"
]
}
/** A call that executes a SQL query via `pg-promise`. */
private class PgPromiseQueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
PgPromiseQueryCall() { this = pgpObject().getMember(pgpQueryMethodName()).getACall() }
/** Gets an argument interpreted as a SQL string, not including raw interpolation variables. */
private DataFlow::Node getADirectQueryArgument() {
result = this.getArgument(0)
or
result = this.getOptionArgument(0, "text")
}
/**
* Gets an interpolation parameter whose value is interpreted literally, or is not escaped appropriately for its context.
*
* For example, the following are raw placeholders: $1:raw, $1^, ${prop}:raw, $(prop)^
*/
private string getARawParameterName() {
exists(string sqlString, string placeholderRegexp, string regexp |
placeholderRegexp = "\\$(\\d+|[{(\\[/]\\w+[})\\]/])" and // For example: $1 or ${prop}
sqlString = this.getADirectQueryArgument().getStringValue()
|
// Match $1:raw or ${prop}:raw
regexp = placeholderRegexp + "(:raw|\\^)" and
result =
sqlString
.regexpFind(regexp, _, _)
.regexpCapture(regexp, 1)
.regexpReplaceAll("[^\\w\\d]", "")
or
// Match $1:value or ${prop}:value unless enclosed by single quotes (:value prevents breaking out of single quotes)
regexp = placeholderRegexp + "(:value|\\#)" and
result =
sqlString
.regexpReplaceAll("'[^']*'", "''")
.regexpFind(regexp, _, _)
.regexpCapture(regexp, 1)
.regexpReplaceAll("[^\\w\\d]", "")
)
}
/** Gets the argument holding the values to plug into placeholders. */
private DataFlow::Node getValues() {
result = this.getArgument(1)
or
result = this.getOptionArgument(0, "values")
}
/** Gets a value that is plugged into a raw placeholder variable, making it a sink for SQL injection. */
private DataFlow::Node getARawValue() {
result = this.getValues() and this.getARawParameterName() = "1" // Special case: if the argument is not an array or object, it's just plugged into $1
or
exists(DataFlow::SourceNode values | values = this.getValues().getALocalSource() |
result = values.getAPropertyWrite(this.getARawParameterName()).getRhs()
or
// Array literals do not have PropWrites with property names so handle them separately,
// and also translate to 0-based indexing.
result =
values.(DataFlow::ArrayCreationNode).getElement(this.getARawParameterName().toInt() - 1)
)
}
override DataFlow::Node getAResult() {
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
}
override DataFlow::Node getAQueryArgument() {
result = this.getADirectQueryArgument()
or
result = this.getARawValue()
}
}
/** An expression that is interpreted as SQL by `pg-promise`. */
class PgPromiseQueryString extends SQL::SqlString {
PgPromiseQueryString() { this = any(PgPromiseQueryCall qc).getAQueryArgument().asExpr() }
}
}
/**
* Provides classes modeling the `sqlite3` package.
*/
private module Sqlite {
/** Gets a reference to the `sqlite3` module. */
DataFlow::SourceNode sqlite() {
result = DataFlow::moduleImport("sqlite3")
or
result = sqlite().getAMemberCall("verbose")
}
/** Gets an expression that constructs a Sqlite database instance. */
DataFlow::SourceNode newDb() {
// new require('sqlite3').Database()
result = sqlite().getAConstructorInvocation("Database")
}
/** Gets a data flow node referring to a Sqlite database instance. */
private DataFlow::SourceNode db(DataFlow::TypeTracker t) {
t.start() and
result = newDb()
or
exists(DataFlow::TypeTracker t2 | result = db(t2).track(t2, t))
}
/** Gets a data flow node referring to a Sqlite database instance. */
DataFlow::SourceNode db() { result = db(DataFlow::TypeTracker::end()) }
/** A call to a Sqlite query method. */
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() { this = db().getAMethodCall(["all", "each", "exec", "get", "prepare", "run"]) }
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) }
}
/** An expression that is passed to the `query` method and hence interpreted as SQL. */
class QueryString extends SQL::SqlString {
QueryString() { this = any(QueryCall qc).getAQueryArgument().asExpr() }
}
}
/**
* Provides classes modeling the `mssql` package.
*/
private module MsSql {
/** Gets a reference to the `mssql` module. */
DataFlow::SourceNode mssql() { result = DataFlow::moduleImport("mssql") }
/** Gets a data flow node referring to a request object. */
private DataFlow::SourceNode request(DataFlow::TypeTracker t) {
t.start() and
(
// new require('mssql').Request()
result = mssql().getAConstructorInvocation("Request")
or
// request.input(...)
result = request().getAMethodCall("input")
)
or
exists(DataFlow::TypeTracker t2 | result = request(t2).track(t2, t))
}
/** Gets a data flow node referring to a request object. */
DataFlow::SourceNode request() { result = request(DataFlow::TypeTracker::end()) }
/** A tagged template evaluated as a query. */
private class QueryTemplateExpr extends DatabaseAccess, DataFlow::ValueNode, DataFlow::SourceNode {
override TaggedTemplateExpr astNode;
QueryTemplateExpr() { mssql().getAPropertyRead("query").flowsToExpr(astNode.getTag()) }
override DataFlow::Node getAResult() {
PromiseFlow::loadStep(this.getALocalUse(), result, Promises::valueProp())
}
override DataFlow::Node getAQueryArgument() {
result = DataFlow::valueNode(astNode.getTemplate().getAnElement())
}
}
/** A call to a MsSql query method. */
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() { this = request().getAMethodCall(["query", "batch"]) }
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) }
}
/** An expression that is passed to a method that interprets it as SQL. */
class QueryString extends SQL::SqlString {
QueryString() {
exists(DatabaseAccess dba | dba instanceof QueryTemplateExpr or dba instanceof QueryCall |
this = dba.getAQueryArgument().asExpr()
)
}
}
/** An element of a query template, which is automatically sanitized. */
class QueryTemplateSanitizer extends SQL::SqlSanitizer {
QueryTemplateSanitizer() {
this = any(QueryTemplateExpr qte).getAQueryArgument().asExpr() and
input = this and
output = this
}
}
/** An expression that is passed as user name or password when creating a client or a pool. */
class Credentials extends CredentialsExpr {
string kind;
Credentials() {
exists(DataFlow::InvokeNode call, string prop |
(
call = mssql().getAMemberCall("connect")
or
call = mssql().getAConstructorInvocation("ConnectionPool")
) and
this = call.getOptionArgument(0, prop).asExpr() and
(
prop = "user" and kind = "user name"
or
prop = "password" and kind = prop
)
)
}
override string getCredentialsKind() { result = kind }
}
}
/**
* Provides classes modeling the `sequelize` package.
*/
private module Sequelize {
/** Gets a node referring to an instance of the `Sequelize` class. */
private DataFlow::SourceNode sequelize(DataFlow::TypeTracker t) {
t.start() and
result = DataFlow::moduleImport("sequelize").getAnInstantiation()
or
exists(DataFlow::TypeTracker t2 | result = sequelize(t2).track(t2, t))
}
/** Gets a node referring to an instance of the `Sequelize` class. */
DataFlow::SourceNode sequelize() { result = sequelize(DataFlow::TypeTracker::end()) }
/** A call to `Sequelize.query`. */
private class QueryCall extends DatabaseAccess, DataFlow::ValueNode {
override MethodCallExpr astNode;
QueryCall() { this = sequelize().getAMethodCall("query") }
override DataFlow::Node getAQueryArgument() {
result = DataFlow::valueNode(astNode.getArgument(0))
}
}
/** An expression that is passed to `Sequelize.query` method and hence interpreted as SQL. */
class QueryString extends SQL::SqlString {
QueryString() { this = any(QueryCall qc).getAQueryArgument().asExpr() }
}
/**
* An expression that is passed as user name or password when creating an instance of the
* `Sequelize` class.
*/
class Credentials extends CredentialsExpr {
string kind;
Credentials() {
exists(NewExpr ne, string prop |
ne = sequelize().asExpr() and
(
this = ne.getArgument(1) and prop = "username"
or
this = ne.getArgument(2) and prop = "password"
or
ne.hasOptionArgument(ne.getNumArgument() - 1, prop, this)
) and
(
prop = "username" and kind = "user name"
or
prop = "password" and kind = prop
)
)
}
}
}
/**
* Provides classes modelling the Google Cloud Spanner library.
*/
private module Spanner {
/**
* Gets a node that refers to the `Spanner` class
*/
DataFlow::SourceNode spanner() {
// older versions
result = DataFlow::moduleImport("@google-cloud/spanner")
or
// newer versions
result = DataFlow::moduleMember("@google-cloud/spanner", "Spanner")
}
/** Gets a data flow node referring to the result of `Spanner()` or `new Spanner()`. */
private DataFlow::SourceNode spannerNew(DataFlow::TypeTracker t) {
t.start() and
result = spanner().getAnInvocation()
or
exists(DataFlow::TypeTracker t2 | result = spannerNew(t2).track(t2, t))
}
/** Gets a data flow node referring to the result of `Spanner()` or `new Spanner()`. */
DataFlow::SourceNode spannerNew() { result = spannerNew(DataFlow::TypeTracker::end()) }
/** Gets a data flow node referring to the result of `.instance()`. */
private DataFlow::SourceNode instance(DataFlow::TypeTracker t) {
t.start() and
result = spannerNew().getAMethodCall("instance")
or
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
}
/** Gets a data flow node referring to the result of `.instance()`. */
DataFlow::SourceNode instance() { result = instance(DataFlow::TypeTracker::end()) }
/** Gets a node that refers to an instance of the `Database` class. */
private DataFlow::SourceNode database(DataFlow::TypeTracker t) {
t.start() and
result = instance().getAMethodCall("database")
or
exists(DataFlow::TypeTracker t2 | result = database(t2).track(t2, t))
}
/** Gets a node that refers to an instance of the `Database` class. */
DataFlow::SourceNode database() { result = database(DataFlow::TypeTracker::end()) }
/** Gets a node that refers to an instance of the `v1.SpannerClient` class. */
private DataFlow::SourceNode v1SpannerClient(DataFlow::TypeTracker t) {
t.start() and
result = spanner().getAPropertyRead("v1").getAPropertyRead("SpannerClient").getAnInstantiation()
or
exists(DataFlow::TypeTracker t2 | result = v1SpannerClient(t2).track(t2, t))
}
/** Gets a node that refers to an instance of the `v1.SpannerClient` class. */
DataFlow::SourceNode v1SpannerClient() { result = v1SpannerClient(DataFlow::TypeTracker::end()) }
/** Gets a node that refers to a transaction object. */
private DataFlow::SourceNode transaction(DataFlow::TypeTracker t) {
t.start() and
result = database().getAMethodCall("runTransaction").getABoundCallbackParameter(0, 1)
or
exists(DataFlow::TypeTracker t2 | result = transaction(t2).track(t2, t))
}
/** Gets a node that refers to a transaction object. */
DataFlow::SourceNode transaction() { result = transaction(DataFlow::TypeTracker::end()) }
/**
* A call to a Spanner method that executes a SQL query.
*/
abstract class SqlExecution extends DatabaseAccess, DataFlow::InvokeNode {
/**
* Gets the position of the query argument; default is zero, which can be overridden
* by subclasses.
*/
int getQueryArgumentPosition() { result = 0 }
override DataFlow::Node getAQueryArgument() {
result = getArgument(getQueryArgumentPosition()) or
result = getOptionArgument(getQueryArgumentPosition(), "sql")
}
}
/**
* A SQL execution that takes the input directly in the first argument or in the `sql` option.
*/
class DatabaseRunCall extends SqlExecution {
DatabaseRunCall() {
this = database().getAMethodCall(["run", "runPartitionedUpdate", "runStream"])
}
}
/**
* A SQL execution that takes an array of SQL strings or { sql: string } objects.
*/
class TransactionRunCall extends SqlExecution {
TransactionRunCall() { this = transaction().getAMethodCall(["run", "runStream", "runUpdate"]) }
}
/**
* A SQL execution that only takes the input in the `sql` option, and do not accept query strings
* directly.
*/
class ExecuteSqlCall extends SqlExecution {
ExecuteSqlCall() {
this = v1SpannerClient().getAMethodCall(["executeSql", "executeStreamingSql"])
}
}
}