Skip to content

Commit 7f07520

Browse files
authored
Merge pull request #4367 from erik-krogh/sql-api
JS: Fixing an API-graph gotcha in `SQL.qll`
2 parents 282d3e8 + bfb653a commit 7f07520

File tree

4 files changed

+84
-45
lines changed

4 files changed

+84
-45
lines changed

javascript/ql/src/semmle/javascript/ApiGraphs.qll

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ module API {
2424
* Gets a data-flow node corresponding to a use of the API component represented by this node.
2525
*
2626
* For example, `require('fs').readFileSync` is a use of the function `readFileSync` from the
27-
* `fs` module, and `require('fs').readFileSync(file)` is a use of the result of that function.
27+
* `fs` module, and `require('fs').readFileSync(file)` is a use of the return of that function.
28+
*
29+
* This includes indirect uses found via data flow, meaning that in
30+
* `f(obj.foo); function f(x) {};` both `obj.foo` and `x` are uses of the `foo` member from `obj`.
2831
*
2932
* As another example, in the assignment `exports.plusOne = (x) => x+1` the two references to
3033
* `x` are uses of the first parameter of `plusOne`.
@@ -35,6 +38,34 @@ module API {
3538
)
3639
}
3740

41+
/**
42+
* Gets an immediate use of the API component represented by this node.
43+
*
44+
* For example, `require('fs').readFileSync` is a an immediate use of the `readFileSync` member
45+
* from the `fs` module.
46+
*
47+
* Unlike `getAUse()`, this predicate only gets the immediate references, not the indirect uses
48+
* found via data flow. This means that in `const x = fs.readFile` only `fs.readFile` is a reference
49+
* to the `readFile` member of `fs`, neither `x` nor any node that `x` flows to is a reference to
50+
* this API component.
51+
*/
52+
DataFlow::SourceNode getAnImmediateUse() { Impl::use(this, result) }
53+
54+
/**
55+
* Gets a call to the function represented by this API component.
56+
*/
57+
DataFlow::CallNode getACall() { result = getReturn().getAnImmediateUse() }
58+
59+
/**
60+
* Gets a `new` call to the function represented by this API component.
61+
*/
62+
DataFlow::NewNode getAnInstantiation() { result = getInstance().getAnImmediateUse() }
63+
64+
/**
65+
* Gets an invocation (with our without `new`) to the function represented by this API component.
66+
*/
67+
DataFlow::InvokeNode getAnInvocation() { result = getACall() or result = getAnInstantiation() }
68+
3869
/**
3970
* Gets a data-flow node corresponding to the right-hand side of a definition of the API
4071
* component represented by this node.

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

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,27 @@ private module MySql {
3131
/** Gets the package name `mysql` or `mysql2`. */
3232
API::Node mysql() { result = API::moduleImport(["mysql", "mysql2"]) }
3333

34-
/** Gets a call to `mysql.createConnection`. */
35-
API::Node createConnection() { result = mysql().getMember("createConnection").getReturn() }
34+
/** Gets a reference to `mysql.createConnection`. */
35+
API::Node createConnection() { result = mysql().getMember("createConnection") }
3636

37-
/** Gets a call to `mysql.createPool`. */
38-
API::Node createPool() { result = mysql().getMember("createPool").getReturn() }
37+
/** Gets a reference to `mysql.createPool`. */
38+
API::Node createPool() { result = mysql().getMember("createPool") }
39+
40+
/** Gets a node that contains a MySQL pool created using `mysql.createPool()`. */
41+
API::Node pool() { result = createPool().getReturn() }
3942

4043
/** Gets a data flow node that contains a freshly created MySQL connection instance. */
4144
API::Node connection() {
42-
result = createConnection()
45+
result = createConnection().getReturn()
4346
or
44-
result = createPool().getMember("getConnection").getParameter(0).getParameter(1)
47+
result = pool().getMember("getConnection").getParameter(0).getParameter(1)
4548
}
4649

4750
/** A call to the MySql `query` method. */
4851
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
4952
QueryCall() {
50-
exists(API::Node recv | recv = createPool() or recv = connection() |
51-
this = recv.getMember("query").getReturn().getAUse()
53+
exists(API::Node recv | recv = pool() or recv = connection() |
54+
this = recv.getMember("query").getACall()
5255
)
5356
}
5457

@@ -63,12 +66,7 @@ private module MySql {
6366
/** A call to the `escape` or `escapeId` method that performs SQL sanitization. */
6467
class EscapingSanitizer extends SQL::SqlSanitizer, MethodCallExpr {
6568
EscapingSanitizer() {
66-
this =
67-
[mysql(), createPool(), connection()]
68-
.getMember(["escape", "escapeId"])
69-
.getReturn()
70-
.getAUse()
71-
.asExpr() and
69+
this = [mysql(), pool(), connection()].getMember(["escape", "escapeId"]).getACall().asExpr() and
7270
input = this.getArgument(0) and
7371
output = this
7472
}
@@ -79,9 +77,9 @@ private module MySql {
7977
string kind;
8078

8179
Credentials() {
82-
exists(API::Node call, string prop |
83-
call in [createConnection(), createPool()] and
84-
call.getAUse().asExpr().(CallExpr).hasOptionArgument(0, prop, this) and
80+
exists(API::Node callee, string prop |
81+
callee in [createConnection(), createPool()] and
82+
this = callee.getParameter(0).getMember(prop).getARhs().asExpr() and
8583
(
8684
prop = "user" and kind = "user name"
8785
or
@@ -98,29 +96,32 @@ private module MySql {
9896
* Provides classes modelling the `pg` package.
9997
*/
10098
private module Postgres {
101-
/** Gets an expression of the form `new require('pg').Client()`. */
102-
API::Node newClient() { result = API::moduleImport("pg").getMember("Client").getInstance() }
99+
/** Gets a reference to the `Client` constructor in the `pg` package, for example `require('pg').Client`. */
100+
API::Node newClient() { result = API::moduleImport("pg").getMember("Client") }
103101

104-
/** Gets a data flow node that holds a freshly created Postgres client instance. */
102+
/** Gets a freshly created Postgres client instance. */
105103
API::Node client() {
106-
result = newClient()
104+
result = newClient().getInstance()
107105
or
108106
// pool.connect(function(err, client) { ... })
109-
result = newPool().getMember("connect").getParameter(0).getParameter(1)
107+
result = pool().getMember("connect").getParameter(0).getParameter(1)
110108
}
111109

112-
/** Gets an expression that constructs a new connection pool. */
110+
/** Gets a constructor that when invoked constructs a new connection pool. */
113111
API::Node newPool() {
114112
// new require('pg').Pool()
115-
result = API::moduleImport("pg").getMember("Pool").getInstance()
113+
result = API::moduleImport("pg").getMember("Pool")
116114
or
117115
// new require('pg-pool')
118-
result = API::moduleImport("pg-pool").getInstance()
116+
result = API::moduleImport("pg-pool")
119117
}
120118

119+
/** Gets an expression that constructs a new connection pool. */
120+
API::Node pool() { result = newPool().getInstance() }
121+
121122
/** A call to the Postgres `query` method. */
122123
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
123-
QueryCall() { this = [client(), newPool()].getMember("query").getReturn().getAUse() }
124+
QueryCall() { this = [client(), pool()].getMember("query").getACall() }
124125

125126
override DataFlow::Node getAQueryArgument() { result = getArgument(0) }
126127
}
@@ -135,9 +136,8 @@ private module Postgres {
135136
string kind;
136137

137138
Credentials() {
138-
exists(DataFlow::InvokeNode call, string prop |
139-
call = [client(), newPool()].getAUse() and
140-
this = call.getOptionArgument(0, prop).asExpr() and
139+
exists(string prop |
140+
this = [newClient(), newPool()].getParameter(0).getMember(prop).getARhs().asExpr() and
141141
(
142142
prop = "user" and kind = "user name"
143143
or
@@ -178,7 +178,7 @@ private module Sqlite {
178178
meth = "prepare" or
179179
meth = "run"
180180
|
181-
this = newDb().getMember(meth).getReturn().getAUse()
181+
this = newDb().getMember(meth).getACall()
182182
)
183183
}
184184

@@ -222,7 +222,7 @@ private module MsSql {
222222

223223
/** A call to a MsSql query method. */
224224
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
225-
QueryCall() { this = request().getMember(["query", "batch"]).getReturn().getAUse() }
225+
QueryCall() { this = request().getMember(["query", "batch"]).getACall() }
226226

227227
override DataFlow::Node getAQueryArgument() { result = getArgument(0) }
228228
}
@@ -250,13 +250,13 @@ private module MsSql {
250250
string kind;
251251

252252
Credentials() {
253-
exists(DataFlow::InvokeNode call, string prop |
253+
exists(API::Node callee, string prop |
254254
(
255-
call = mssql().getMember("connect").getReturn().getAUse()
255+
callee = mssql().getMember("connect")
256256
or
257-
call = mssql().getMember("ConnectionPool").getInstance().getAUse()
257+
callee = mssql().getMember("ConnectionPool")
258258
) and
259-
this = call.getOptionArgument(0, prop).asExpr() and
259+
this = callee.getParameter(0).getMember(prop).getARhs().asExpr() and
260260
(
261261
prop = "user" and kind = "user name"
262262
or
@@ -281,7 +281,7 @@ private module Sequelize {
281281

282282
/** A call to `Sequelize.query`. */
283283
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
284-
QueryCall() { this = newSequelize().getMember("query").getReturn().getAUse() }
284+
QueryCall() { this = newSequelize().getMember("query").getACall() }
285285

286286
override DataFlow::Node getAQueryArgument() { result = getArgument(0) }
287287
}
@@ -300,7 +300,7 @@ private module Sequelize {
300300

301301
Credentials() {
302302
exists(NewExpr ne, string prop |
303-
ne = newSequelize().getAUse().asExpr() and
303+
ne = sequelize().getAnInstantiation().asExpr() and
304304
(
305305
this = ne.getArgument(1) and prop = "username"
306306
or
@@ -378,8 +378,7 @@ private module Spanner {
378378
*/
379379
class DatabaseRunCall extends SqlExecution {
380380
DatabaseRunCall() {
381-
this =
382-
database().getMember(["run", "runPartitionedUpdate", "runStream"]).getReturn().getAUse()
381+
this = database().getMember(["run", "runPartitionedUpdate", "runStream"]).getACall()
383382
}
384383
}
385384

@@ -388,7 +387,7 @@ private module Spanner {
388387
*/
389388
class TransactionRunCall extends SqlExecution {
390389
TransactionRunCall() {
391-
this = transaction().getMember(["run", "runStream", "runUpdate"]).getReturn().getAUse()
390+
this = transaction().getMember(["run", "runStream", "runUpdate"]).getACall()
392391
}
393392
}
394393

@@ -397,8 +396,7 @@ private module Spanner {
397396
*/
398397
class ExecuteSqlCall extends SqlExecution {
399398
ExecuteSqlCall() {
400-
this =
401-
v1SpannerClient().getMember(["executeSql", "executeStreamingSql"]).getReturn().getAUse()
399+
this = v1SpannerClient().getMember(["executeSql", "executeStreamingSql"]).getACall()
402400
}
403401

404402
override DataFlow::Node getAQueryArgument() {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
| mssql2.js:5:15:5:34 | 'select 1 as number' |
44
| mssql2.js:13:15:13:66 | 'create ... table' |
55
| mssql2.js:22:24:22:43 | 'select 1 as number' |
6+
| mssql2.js:29:30:29:81 | 'create ... table' |
67
| mysql1.js:13:18:13:43 | 'SELECT ... lution' |
78
| mysql1.js:18:18:22:1 | {\\n s ... vid']\\n} |
89
| mysql1a.js:17:18:17:43 | 'SELECT ... lution' |

javascript/ql/test/library-tests/frameworks/SQL/mssql2.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Adapted from https://github.com/tediousjs/node-mssql#readme
22
const sql = require('mssql')
3-
3+
44
const request = new sql.Request()
55
request.query('select 1 as number', (err, result) => {
66
// ... error checks
@@ -19,7 +19,16 @@ class C {
1919
this.req = req;
2020
}
2121
send() {
22-
this.req.query('select 1 as number', (err, result) => {})
22+
this.req.query('select 1 as number', (err, result) => { })
2323
}
2424
}
2525
new C(new sql.Request());
26+
27+
var obj = {
28+
foo: function () {
29+
return request.batch('create procedure #temporary as select * from table', (err, result) => {
30+
// ... error checks
31+
})
32+
}
33+
}
34+
obj.foo("foo", "bar", "baz"); // An API-graphs gotcha: "baz" should not be considered a `SqlString`

0 commit comments

Comments
 (0)