Skip to content

Commit 6cceb73

Browse files
authored
Merge pull request github#5553 from asgerf/js/pg-promise
Approved by esbena
2 parents d2b991b + 67ad6d9 commit 6cceb73

File tree

6 files changed

+397
-11
lines changed

6 files changed

+397
-11
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* SQL injection sinks from the `pg-promise` library are now recognized.

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

Lines changed: 148 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,31 +93,43 @@ private module MySql {
9393
}
9494

9595
/**
96-
* Provides classes modelling the `pg` package.
96+
* Provides classes modelling the PostgreSQL packages, such as `pg` and `pg-promise`.
9797
*/
9898
private module Postgres {
99+
API::Node pg() {
100+
result = API::moduleImport("pg")
101+
or
102+
result = pgpMain().getMember("pg")
103+
}
104+
99105
/** 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") }
106+
API::Node newClient() { result = pg().getMember("Client") }
101107

102108
/** Gets a freshly created Postgres client instance. */
103109
API::Node client() {
104110
result = newClient().getInstance()
105111
or
106112
// pool.connect(function(err, client) { ... })
107113
result = pool().getMember("connect").getParameter(0).getParameter(1)
114+
or
115+
result = pgpConnection().getMember("client")
108116
}
109117

110118
/** Gets a constructor that when invoked constructs a new connection pool. */
111119
API::Node newPool() {
112120
// new require('pg').Pool()
113-
result = API::moduleImport("pg").getMember("Pool")
121+
result = pg().getMember("Pool")
114122
or
115123
// new require('pg-pool')
116124
result = API::moduleImport("pg-pool")
117125
}
118126

119-
/** Gets an expression that constructs a new connection pool. */
120-
API::Node pool() { result = newPool().getInstance() }
127+
/** Gets an API node that refers to a connection pool. */
128+
API::Node pool() {
129+
result = newPool().getInstance()
130+
or
131+
result = pgpDatabase().getMember("$pool")
132+
}
121133

122134
/** A call to the Postgres `query` method. */
123135
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
@@ -137,17 +149,142 @@ private module Postgres {
137149

138150
Credentials() {
139151
exists(string prop |
140-
this = [newClient(), newPool()].getParameter(0).getMember(prop).getARhs().asExpr() and
141-
(
142-
prop = "user" and kind = "user name"
143-
or
144-
prop = "password" and kind = prop
145-
)
152+
this = [newClient(), newPool()].getParameter(0).getMember(prop).getARhs().asExpr()
153+
or
154+
this = pgPromise().getParameter(0).getMember(prop).getARhs().asExpr()
155+
|
156+
prop = "user" and kind = "user name"
157+
or
158+
prop = "password" and kind = prop
146159
)
147160
}
148161

149162
override string getCredentialsKind() { result = kind }
150163
}
164+
165+
/** Gets a node referring to the `pg-promise` library (which is not itself a Promise). */
166+
API::Node pgPromise() { result = API::moduleImport("pg-promise") }
167+
168+
/** Gets an initialized `pg-promise` library. */
169+
API::Node pgpMain() {
170+
result = pgPromise().getReturn()
171+
or
172+
result = API::Node::ofType("pg-promise", "IMain")
173+
}
174+
175+
/** Gets a database from `pg-promise`. */
176+
API::Node pgpDatabase() {
177+
result = pgpMain().getReturn()
178+
or
179+
result = API::Node::ofType("pg-promise", "IDatabase")
180+
}
181+
182+
/** Gets a connection created from a `pg-promise` database. */
183+
API::Node pgpConnection() {
184+
result = pgpDatabase().getMember("connect").getReturn().getPromised()
185+
or
186+
result = API::Node::ofType("pg-promise", "IConnected")
187+
}
188+
189+
/** Gets a `pg-promise` task object. */
190+
API::Node pgpTask() {
191+
exists(API::Node taskMethod |
192+
taskMethod = pgpObject().getMember(["task", "taskIf", "tx", "txIf"])
193+
|
194+
result = taskMethod.getParameter([0, 1]).getParameter(0)
195+
or
196+
result = taskMethod.getParameter(0).getMember("cnd").getParameter(0)
197+
)
198+
or
199+
result = API::Node::ofType("pg-promise", "ITask")
200+
}
201+
202+
/** Gets a `pg-promise` object which supports querying (database, connection, or task). */
203+
API::Node pgpObject() {
204+
result = [pgpDatabase(), pgpConnection(), pgpTask()]
205+
or
206+
result = API::Node::ofType("pg-promise", "IBaseProtocol")
207+
}
208+
209+
private string pgpQueryMethodName() {
210+
result =
211+
[
212+
"any", "each", "many", "manyOrNone", "map", "multi", "multiResult", "none", "one",
213+
"oneOrNone", "query", "result"
214+
]
215+
}
216+
217+
/** A call that executes a SQL query via `pg-promise`. */
218+
private class PgPromiseQueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
219+
PgPromiseQueryCall() { this = pgpObject().getMember(pgpQueryMethodName()).getACall() }
220+
221+
/** Gets an argument interpreted as a SQL string, not including raw interpolation variables. */
222+
private DataFlow::Node getADirectQueryArgument() {
223+
result = getArgument(0)
224+
or
225+
result = getOptionArgument(0, "text")
226+
}
227+
228+
/**
229+
* Gets an interpolation parameter whose value is interpreted literally, or is not escaped appropriately for its context.
230+
*
231+
* For example, the following are raw placeholders: $1:raw, $1^, ${prop}:raw, $(prop)^
232+
*/
233+
private string getARawParameterName() {
234+
exists(string sqlString, string placeholderRegexp, string regexp |
235+
placeholderRegexp = "\\$(\\d+|[{(\\[/]\\w+[})\\]/])" and // For example: $1 or ${prop}
236+
sqlString = getADirectQueryArgument().getStringValue()
237+
|
238+
// Match $1:raw or ${prop}:raw
239+
regexp = placeholderRegexp + "(:raw|\\^)" and
240+
result =
241+
sqlString
242+
.regexpFind(regexp, _, _)
243+
.regexpCapture(regexp, 1)
244+
.regexpReplaceAll("[^\\w\\d]", "")
245+
or
246+
// Match $1:value or ${prop}:value unless enclosed by single quotes (:value prevents breaking out of single quotes)
247+
regexp = placeholderRegexp + "(:value|\\#)" and
248+
result =
249+
sqlString
250+
.regexpReplaceAll("'[^']*'", "''")
251+
.regexpFind(regexp, _, _)
252+
.regexpCapture(regexp, 1)
253+
.regexpReplaceAll("[^\\w\\d]", "")
254+
)
255+
}
256+
257+
/** Gets the argument holding the values to plug into placeholders. */
258+
private DataFlow::Node getValues() {
259+
result = getArgument(1)
260+
or
261+
result = getOptionArgument(0, "values")
262+
}
263+
264+
/** Gets a value that is plugged into a raw placeholder variable, making it a sink for SQL injection. */
265+
private DataFlow::Node getARawValue() {
266+
result = getValues() and getARawParameterName() = "1" // Special case: if the argument is not an array or object, it's just plugged into $1
267+
or
268+
exists(DataFlow::SourceNode values | values = getValues().getALocalSource() |
269+
result = values.getAPropertyWrite(getARawParameterName()).getRhs()
270+
or
271+
// Array literals do not have PropWrites with property names so handle them separately,
272+
// and also translate to 0-based indexing.
273+
result = values.(DataFlow::ArrayCreationNode).getElement(getARawParameterName().toInt() - 1)
274+
)
275+
}
276+
277+
override DataFlow::Node getAQueryArgument() {
278+
result = getADirectQueryArgument()
279+
or
280+
result = getARawValue()
281+
}
282+
}
283+
284+
/** An expression that is interpreted as SQL by `pg-promise`. */
285+
class PgPromiseQueryString extends SQL::SqlString {
286+
PgPromiseQueryString() { this = any(PgPromiseQueryCall qc).getAQueryArgument().asExpr() }
287+
}
151288
}
152289

153290
/**

javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,28 @@
3737
| mongoose.js:97:2:97:52 | Documen ... query)) |
3838
| mongoose.js:99:2:99:50 | Documen ... query)) |
3939
| mongoose.js:113:2:113:53 | Documen ... () { }) |
40+
| pg-promise-types.ts:8:5:8:22 | this.db.one(taint) |
41+
| pg-promise.js:9:3:9:15 | db.any(query) |
42+
| pg-promise.js:10:3:10:16 | db.many(query) |
43+
| pg-promise.js:11:3:11:22 | db.manyOrNone(query) |
44+
| pg-promise.js:12:3:12:15 | db.map(query) |
45+
| pg-promise.js:13:3:13:17 | db.multi(query) |
46+
| pg-promise.js:14:3:14:23 | db.mult ... (query) |
47+
| pg-promise.js:15:3:15:16 | db.none(query) |
48+
| pg-promise.js:16:3:16:15 | db.one(query) |
49+
| pg-promise.js:17:3:17:21 | db.oneOrNone(query) |
50+
| pg-promise.js:18:3:18:17 | db.query(query) |
51+
| pg-promise.js:19:3:19:18 | db.result(query) |
52+
| pg-promise.js:21:3:23:4 | db.one( ... OK\\n }) |
53+
| pg-promise.js:24:3:27:4 | db.one( ... OK\\n }) |
54+
| pg-promise.js:28:3:31:4 | db.one( ... er\\n }) |
55+
| pg-promise.js:32:3:35:4 | db.one( ... OK\\n }) |
56+
| pg-promise.js:36:3:43:4 | db.one( ... ]\\n }) |
57+
| pg-promise.js:44:3:50:4 | db.one( ... }\\n }) |
58+
| pg-promise.js:51:3:58:4 | db.one( ... }\\n }) |
59+
| pg-promise.js:60:14:60:25 | t.one(query) |
60+
| pg-promise.js:63:17:63:28 | t.one(query) |
61+
| pg-promise.js:64:10:64:21 | t.one(query) |
4062
| socketio.js:11:5:11:54 | db.run( ... ndle}`) |
4163
| tst2.js:7:3:7:62 | sql.que ... ms.id}` |
4264
| tst2.js:9:3:9:85 | new sql ... + "'") |

0 commit comments

Comments
 (0)