Skip to content

Commit 6fd4a8a

Browse files
authored
Merge pull request github#5567 from asgerf/js/sql-models
Approved by esbena
2 parents be2fe6e + 57784dc commit 6fd4a8a

File tree

17 files changed

+274
-56
lines changed

17 files changed

+274
-56
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
lgtm,codescanning
2+
* The SQL library models for `mysql`, `mysql2`, `mssql`, `pg`, `sqlite3`, `sequelize`, and `@google-cloud/spanner` have improved,
3+
leading to more SQL injection sinks.

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

Lines changed: 143 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -28,30 +28,52 @@ module SQL {
2828
* Provides classes modelling the (API compatible) `mysql` and `mysql2` packages.
2929
*/
3030
private module MySql {
31+
private string moduleName() { result = ["mysql", "mysql2", "mysql2/promise"] }
32+
3133
/** Gets the package name `mysql` or `mysql2`. */
32-
API::Node mysql() { result = API::moduleImport(["mysql", "mysql2"]) }
34+
API::Node mysql() { result = API::moduleImport(moduleName()) }
3335

3436
/** Gets a reference to `mysql.createConnection`. */
35-
API::Node createConnection() { result = mysql().getMember("createConnection") }
37+
API::Node createConnection() {
38+
result = mysql().getMember(["createConnection", "createConnectionPromise"])
39+
}
3640

3741
/** Gets a reference to `mysql.createPool`. */
38-
API::Node createPool() { result = mysql().getMember("createPool") }
42+
API::Node createPool() { result = mysql().getMember(["createPool", "createPoolCluster"]) }
3943

4044
/** Gets a node that contains a MySQL pool created using `mysql.createPool()`. */
41-
API::Node pool() { result = createPool().getReturn() }
45+
API::Node pool() {
46+
result = createPool().getReturn()
47+
or
48+
result = pool().getMember("on").getReturn()
49+
or
50+
result = API::Node::ofType(moduleName(), ["Pool", "PoolCluster"])
51+
}
4252

4353
/** Gets a data flow node that contains a freshly created MySQL connection instance. */
4454
API::Node connection() {
4555
result = createConnection().getReturn()
4656
or
57+
result = createConnection().getReturn().getPromised()
58+
or
4759
result = pool().getMember("getConnection").getParameter(0).getParameter(1)
60+
or
61+
result = pool().getMember("getConnection").getPromised()
62+
or
63+
exists(API::CallNode call |
64+
call = pool().getMember("on").getACall() and
65+
call.getArgument(0).getStringValue() = ["connection", "acquire", "release"] and
66+
result = call.getParameter(1).getParameter(0)
67+
)
68+
or
69+
result = API::Node::ofType(moduleName(), ["Connection", "PoolConnection"])
4870
}
4971

5072
/** A call to the MySql `query` method. */
5173
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
5274
QueryCall() {
5375
exists(API::Node recv | recv = pool() or recv = connection() |
54-
this = recv.getMember("query").getACall()
76+
this = recv.getMember(["query", "execute"]).getACall()
5577
)
5678
}
5779

@@ -112,7 +134,20 @@ private module Postgres {
112134
// pool.connect(function(err, client) { ... })
113135
result = pool().getMember("connect").getParameter(0).getParameter(1)
114136
or
137+
// await pool.connect()
138+
result = pool().getMember("connect").getReturn().getPromised()
139+
or
115140
result = pgpConnection().getMember("client")
141+
or
142+
exists(API::CallNode call |
143+
call = pool().getMember("on").getACall() and
144+
call.getArgument(0).getStringValue() = ["connect", "acquire"] and
145+
result = call.getParameter(1).getParameter(0)
146+
)
147+
or
148+
result = client().getMember("on").getReturn()
149+
or
150+
result = API::Node::ofType("pg", ["Client", "PoolClient"])
116151
}
117152

118153
/** Gets a constructor that when invoked constructs a new connection pool. */
@@ -129,6 +164,10 @@ private module Postgres {
129164
result = newPool().getInstance()
130165
or
131166
result = pgpDatabase().getMember("$pool")
167+
or
168+
result = pool().getMember("on").getReturn()
169+
or
170+
result = API::Node::ofType("pg", "Pool")
132171
}
133172

134173
/** A call to the Postgres `query` method. */
@@ -140,7 +179,11 @@ private module Postgres {
140179

141180
/** An expression that is passed to the `query` method and hence interpreted as SQL. */
142181
class QueryString extends SQL::SqlString {
143-
QueryString() { this = any(QueryCall qc).getAQueryArgument().asExpr() }
182+
QueryString() {
183+
this = any(QueryCall qc).getAQueryArgument().asExpr()
184+
or
185+
this = API::moduleImport("pg-cursor").getParameter(0).getARhs().asExpr()
186+
}
144187
}
145188

146189
/** An expression that is passed as user name or password when creating a client or a pool. */
@@ -299,24 +342,17 @@ private module Sqlite {
299342
}
300343

301344
/** Gets an expression that constructs a Sqlite database instance. */
302-
API::Node newDb() {
345+
API::Node database() {
303346
// new require('sqlite3').Database()
304347
result = sqlite().getMember("Database").getInstance()
348+
or
349+
result = API::Node::ofType("sqlite3", "Database")
305350
}
306351

307352
/** A call to a Sqlite query method. */
308353
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
309354
QueryCall() {
310-
exists(string meth |
311-
meth = "all" or
312-
meth = "each" or
313-
meth = "exec" or
314-
meth = "get" or
315-
meth = "prepare" or
316-
meth = "run"
317-
|
318-
this = newDb().getMember(meth).getACall()
319-
)
355+
this = database().getMember(["all", "each", "exec", "get", "prepare", "run"]).getACall()
320356
}
321357

322358
override DataFlow::Node getAQueryArgument() { result = getArgument(0) }
@@ -335,15 +371,32 @@ private module MsSql {
335371
/** Gets a reference to the `mssql` module. */
336372
API::Node mssql() { result = API::moduleImport("mssql") }
337373

338-
/** Gets an expression that creates a request object. */
374+
/** Gets a node referring to an instance of the given class. */
375+
API::Node mssqlClass(string name) {
376+
result = mssql().getMember(name).getInstance()
377+
or
378+
result = API::Node::ofType("mssql", name)
379+
}
380+
381+
/** Gets an API node referring to a Request object. */
339382
API::Node request() {
340-
// new require('mssql').Request()
341-
result = mssql().getMember("Request").getInstance()
383+
result = mssqlClass("Request")
384+
or
385+
result = request().getMember(["input", "replaceInput", "output", "replaceOutput"]).getReturn()
342386
or
343-
// request.input(...)
344-
result = request().getMember("input").getReturn()
387+
result = [transaction(), pool()].getMember("request").getReturn()
345388
}
346389

390+
/** Gets an API node referring to a Transaction object. */
391+
API::Node transaction() {
392+
result = mssqlClass("Transaction")
393+
or
394+
result = pool().getMember("transaction").getReturn()
395+
}
396+
397+
/** Gets a API node referring to a ConnectionPool object. */
398+
API::Node pool() { result = mssqlClass("ConnectionPool") }
399+
347400
/** A tagged template evaluated as a query. */
348401
private class QueryTemplateExpr extends DatabaseAccess, DataFlow::ValueNode {
349402
override TaggedTemplateExpr astNode;
@@ -359,7 +412,7 @@ private module MsSql {
359412

360413
/** A call to a MsSql query method. */
361414
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
362-
QueryCall() { this = request().getMember(["query", "batch"]).getACall() }
415+
QueryCall() { this = [mssql(), request()].getMember(["query", "batch"]).getACall() }
363416

364417
override DataFlow::Node getAQueryArgument() { result = getArgument(0) }
365418
}
@@ -410,22 +463,34 @@ private module MsSql {
410463
* Provides classes modelling the `sequelize` package.
411464
*/
412465
private module Sequelize {
413-
/** Gets an import of the `sequelize` module. */
414-
API::Node sequelize() { result = API::moduleImport("sequelize") }
466+
/** Gets an import of the `sequelize` module or one that re-exports it. */
467+
API::Node sequelize() { result = API::moduleImport(["sequelize", "sequelize-typescript"]) }
415468

416469
/** Gets an expression that creates an instance of the `Sequelize` class. */
417-
API::Node newSequelize() { result = sequelize().getInstance() }
470+
API::Node instance() {
471+
result = [sequelize(), sequelize().getMember("Sequelize")].getInstance()
472+
or
473+
result = API::Node::ofType(["sequelize", "sequelize-typescript"], ["Sequelize", "default"])
474+
}
418475

419476
/** A call to `Sequelize.query`. */
420477
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
421-
QueryCall() { this = newSequelize().getMember("query").getACall() }
478+
QueryCall() { this = instance().getMember("query").getACall() }
422479

423-
override DataFlow::Node getAQueryArgument() { result = getArgument(0) }
480+
override DataFlow::Node getAQueryArgument() {
481+
result = getArgument(0)
482+
or
483+
result = getOptionArgument(0, "query")
484+
}
424485
}
425486

426487
/** An expression that is passed to `Sequelize.query` method and hence interpreted as SQL. */
427488
class QueryString extends SQL::SqlString {
428-
QueryString() { this = any(QueryCall qc).getAQueryArgument().asExpr() }
489+
QueryString() {
490+
this = any(QueryCall qc).getAQueryArgument().asExpr()
491+
or
492+
this = sequelize().getMember(["literal", "asIs"]).getParameter(0).getARhs().asExpr()
493+
}
429494
}
430495

431496
/**
@@ -478,68 +543,90 @@ private module Spanner {
478543
API::Node database() {
479544
result =
480545
spanner().getReturn().getMember("instance").getReturn().getMember("database").getReturn()
546+
or
547+
result = API::Node::ofType("@google-cloud/spanner", "Database")
481548
}
482549

483550
/**
484551
* Gets a node that refers to an instance of the `v1.SpannerClient` class.
485552
*/
486553
API::Node v1SpannerClient() {
487554
result = spanner().getMember("v1").getMember("SpannerClient").getInstance()
555+
or
556+
result = API::Node::ofType("@google-cloud/spanner", "v1.SpannerClient")
488557
}
489558

490559
/**
491560
* Gets a node that refers to a transaction object.
492561
*/
493562
API::Node transaction() {
494-
result = database().getMember("runTransaction").getParameter(0).getParameter(1)
563+
result =
564+
database()
565+
.getMember(["runTransaction", "runTransactionAsync"])
566+
.getParameter([0, 1])
567+
.getParameter(1)
568+
or
569+
result = API::Node::ofType("@google-cloud/spanner", "Transaction")
570+
}
571+
572+
/** Gets an API node referring to a `BatchTransaction` object. */
573+
API::Node batchTransaction() {
574+
result = database().getMember("batchTransaction").getReturn()
575+
or
576+
result = database().getMember("createBatchTransaction").getReturn().getPromised()
577+
or
578+
result = API::Node::ofType("@google-cloud/spanner", "BatchTransaction")
495579
}
496580

497581
/**
498582
* A call to a Spanner method that executes a SQL query.
499583
*/
500-
abstract class SqlExecution extends DatabaseAccess, DataFlow::InvokeNode {
501-
/**
502-
* Gets the position of the query argument; default is zero, which can be overridden
503-
* by subclasses.
504-
*/
505-
int getQueryArgumentPosition() { result = 0 }
506-
507-
override DataFlow::Node getAQueryArgument() {
508-
result = getArgument(getQueryArgumentPosition()) or
509-
result = getOptionArgument(getQueryArgumentPosition(), "sql")
510-
}
511-
}
584+
abstract class SqlExecution extends DatabaseAccess, DataFlow::InvokeNode { }
512585

513586
/**
514-
* A call to `Database.run`, `Database.runPartitionedUpdate` or `Database.runStream`.
587+
* A SQL execution that takes the input directly in the first argument or in the `sql` option.
515588
*/
516-
class DatabaseRunCall extends SqlExecution {
517-
DatabaseRunCall() {
589+
class SqlExecutionDirect extends SqlExecution {
590+
SqlExecutionDirect() {
518591
this = database().getMember(["run", "runPartitionedUpdate", "runStream"]).getACall()
592+
or
593+
this = transaction().getMember(["run", "runStream", "runUpdate"]).getACall()
594+
or
595+
this = batchTransaction().getMember("createQueryPartitions").getACall()
596+
}
597+
598+
override DataFlow::Node getAQueryArgument() {
599+
result = getArgument(0)
600+
or
601+
result = getOptionArgument(0, "sql")
519602
}
520603
}
521604

522605
/**
523-
* A call to `Transaction.run`, `Transaction.runStream` or `Transaction.runUpdate`.
606+
* A SQL execution that takes an array of SQL strings or { sql: string } objects.
524607
*/
525-
class TransactionRunCall extends SqlExecution {
526-
TransactionRunCall() {
527-
this = transaction().getMember(["run", "runStream", "runUpdate"]).getACall()
608+
class SqlExecutionBatch extends SqlExecution, API::CallNode {
609+
SqlExecutionBatch() { this = transaction().getMember("batchUpdate").getACall() }
610+
611+
override DataFlow::Node getAQueryArgument() {
612+
// just use the whole array as the query argument, as arrays becomes tainted if one of the elements
613+
// are tainted
614+
result = getArgument(0)
615+
or
616+
result = getParameter(0).getUnknownMember().getMember("sql").getARhs()
528617
}
529618
}
530619

531620
/**
532-
* A call to `v1.SpannerClient.executeSql` or `v1.SpannerClient.executeStreamingSql`.
621+
* A SQL execution that only takes the input in the `sql` option, and do not accept query strings
622+
* directly.
533623
*/
534-
class ExecuteSqlCall extends SqlExecution {
535-
ExecuteSqlCall() {
624+
class SqlExecutionWithOption extends SqlExecution {
625+
SqlExecutionWithOption() {
536626
this = v1SpannerClient().getMember(["executeSql", "executeStreamingSql"]).getACall()
537627
}
538628

539-
override DataFlow::Node getAQueryArgument() {
540-
// `executeSql` and `executeStreamingSql` do not accept query strings directly
541-
result = getOptionArgument(0, "sql")
542-
}
629+
override DataFlow::Node getAQueryArgument() { result = getOptionArgument(0, "sql") }
543630
}
544631

545632
/**

javascript/ql/test/library-tests/frameworks/SQL/Credentials.expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
| mysql1.js:7:14:7:21 | 'secret' | password |
77
| mysql1a.js:10:9:10:12 | 'me' | user name |
88
| mysql1a.js:11:13:11:20 | 'secret' | password |
9+
| mysql2-promise.js:8:9:8:14 | 'root' | user name |
910
| mysql2.js:7:21:7:25 | 'bob' | user name |
1011
| mysql2.js:8:21:8:28 | 'secret' | password |
1112
| mysql2tst.js:8:9:8:14 | 'root' | user name |

0 commit comments

Comments
 (0)