diff --git a/javascript/change-notes/2020-11-30-nosql.md b/javascript/change-notes/2020-11-30-nosql.md new file mode 100644 index 00000000000..1aaee78edcf --- /dev/null +++ b/javascript/change-notes/2020-11-30-nosql.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* The query "Database query built from user-controlled sources" (`js/sql-injection`) has been improved to recognize more Mongoose APIs that may interpret untrusted user input as a query. diff --git a/javascript/ql/src/Security/CWE-089/SqlInjection.qhelp b/javascript/ql/src/Security/CWE-089/SqlInjection.qhelp index db86f591e85..8cdc2419d47 100644 --- a/javascript/ql/src/Security/CWE-089/SqlInjection.qhelp +++ b/javascript/ql/src/Security/CWE-089/SqlInjection.qhelp @@ -17,6 +17,11 @@ Most database connector libraries offer a way of safely embedding untrusted data into a query by means of query parameters or prepared statements.

+

+For NoSQL queries, make use of an operator like MongoDB's $eq +to ensure that untrusted data is interpreted as a literal value and not as +a query object. +

@@ -52,5 +57,6 @@ immune to injection attacks.
  • Wikipedia: SQL injection.
  • +
  • MongoDB: $eq operator.
  • diff --git a/javascript/ql/src/semmle/javascript/frameworks/NoSQL.qll b/javascript/ql/src/semmle/javascript/frameworks/NoSQL.qll index 31671cbc423..0002a057b8e 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/NoSQL.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/NoSQL.qll @@ -237,9 +237,17 @@ private module Mongoose { // implement lots of the MongoDB collection interface MongoDB::CollectionMethodSignatures::interpretsArgumentAsQuery(name, n) or - name = "findByIdAndUpdate" and n = 1 + name = "find" + ["ById", "One"] + "AndUpdate" and n = 1 or - name = "where" and n = 0 + name in ["delete" + ["Many", "One"], "geoSearch", "remove", "replaceOne", "where"] and + n = 0 + or + name in [ + "find" + ["", "ById", "One"], + "find" + ["ById", "One"] + "And" + ["Delete", "Remove", "Update"], + "update" + ["", "Many", "One"] + ] and + n = 0 } /** @@ -262,6 +270,7 @@ private module Mongoose { name = "findOneAndReplace" or name = "findOneAndUpdate" or name = "geosearch" or + name = "remove" or name = "replaceOne" or name = "update" or name = "updateMany" or diff --git a/javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected b/javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected index 5b0956053e5..d17ff8d1cc4 100644 --- a/javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected +++ b/javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected @@ -32,6 +32,7 @@ | mongoose.js:96:2:96:52 | Documen ... query)) | | mongoose.js:97:2:97:52 | Documen ... query)) | | mongoose.js:99:2:99:50 | Documen ... query)) | +| mongoose.js:113:2:113:53 | Documen ... () { }) | | socketio.js:11:5:11:54 | db.run( ... ndle}`) | | tst2.js:7:3:7:62 | sql.que ... ms.id}` | | tst2.js:9:3:9:85 | new sql ... + "'") | diff --git a/javascript/ql/test/query-tests/Security/CWE-089/untyped/SqlInjection.expected b/javascript/ql/test/query-tests/Security/CWE-089/untyped/SqlInjection.expected index 2577a24fb97..e33f30d01ba 100644 --- a/javascript/ql/test/query-tests/Security/CWE-089/untyped/SqlInjection.expected +++ b/javascript/ql/test/query-tests/Security/CWE-089/untyped/SqlInjection.expected @@ -138,6 +138,45 @@ nodes | mongoose.js:96:46:96:50 | query | | mongoose.js:111:14:111:18 | query | | mongoose.js:111:14:111:18 | query | +| mongoose.js:113:31:113:35 | query | +| mongoose.js:113:31:113:35 | query | +| mongoose.js:115:6:115:22 | id | +| mongoose.js:115:11:115:22 | req.query.id | +| mongoose.js:115:11:115:22 | req.query.id | +| mongoose.js:115:25:115:45 | cond | +| mongoose.js:115:32:115:45 | req.query.cond | +| mongoose.js:115:32:115:45 | req.query.cond | +| mongoose.js:116:22:116:25 | cond | +| mongoose.js:116:22:116:25 | cond | +| mongoose.js:117:21:117:24 | cond | +| mongoose.js:117:21:117:24 | cond | +| mongoose.js:118:21:118:24 | cond | +| mongoose.js:118:21:118:24 | cond | +| mongoose.js:119:18:119:21 | cond | +| mongoose.js:119:18:119:21 | cond | +| mongoose.js:120:22:120:25 | cond | +| mongoose.js:120:22:120:25 | cond | +| mongoose.js:121:16:121:19 | cond | +| mongoose.js:121:16:121:19 | cond | +| mongoose.js:122:19:122:22 | cond | +| mongoose.js:122:19:122:22 | cond | +| mongoose.js:123:20:123:21 | id | +| mongoose.js:123:20:123:21 | id | +| mongoose.js:124:28:124:31 | cond | +| mongoose.js:124:28:124:31 | cond | +| mongoose.js:125:28:125:31 | cond | +| mongoose.js:125:28:125:31 | cond | +| mongoose.js:126:28:126:31 | cond | +| mongoose.js:126:28:126:31 | cond | +| mongoose.js:127:18:127:21 | cond | +| mongoose.js:127:18:127:21 | cond | +| mongoose.js:128:22:128:25 | cond | +| mongoose.js:128:22:128:25 | cond | +| mongoose.js:129:21:129:24 | cond | +| mongoose.js:129:21:129:24 | cond | +| mongoose.js:130:16:130:26 | { _id: id } | +| mongoose.js:130:16:130:26 | { _id: id } | +| mongoose.js:130:23:130:24 | id | | mongooseJsonParse.js:19:11:19:20 | query | | mongooseJsonParse.js:19:19:19:20 | {} | | mongooseJsonParse.js:20:19:20:44 | JSON.pa ... y.data) | @@ -369,6 +408,8 @@ edges | mongoose.js:20:11:20:20 | query | mongoose.js:96:46:96:50 | query | | mongoose.js:20:11:20:20 | query | mongoose.js:111:14:111:18 | query | | mongoose.js:20:11:20:20 | query | mongoose.js:111:14:111:18 | query | +| mongoose.js:20:11:20:20 | query | mongoose.js:113:31:113:35 | query | +| mongoose.js:20:11:20:20 | query | mongoose.js:113:31:113:35 | query | | mongoose.js:20:19:20:20 | {} | mongoose.js:20:11:20:20 | query | | mongoose.js:21:19:21:26 | req.body | mongoose.js:21:19:21:32 | req.body.title | | mongoose.js:21:19:21:26 | req.body | mongoose.js:21:19:21:32 | req.body.title | @@ -437,8 +478,45 @@ edges | mongoose.js:21:19:21:32 | req.body.title | mongoose.js:96:46:96:50 | query | | mongoose.js:21:19:21:32 | req.body.title | mongoose.js:111:14:111:18 | query | | mongoose.js:21:19:21:32 | req.body.title | mongoose.js:111:14:111:18 | query | +| mongoose.js:21:19:21:32 | req.body.title | mongoose.js:113:31:113:35 | query | +| mongoose.js:21:19:21:32 | req.body.title | mongoose.js:113:31:113:35 | query | | mongoose.js:24:25:24:29 | query | mongoose.js:24:24:24:30 | [query] | | mongoose.js:24:25:24:29 | query | mongoose.js:24:24:24:30 | [query] | +| mongoose.js:115:6:115:22 | id | mongoose.js:123:20:123:21 | id | +| mongoose.js:115:6:115:22 | id | mongoose.js:123:20:123:21 | id | +| mongoose.js:115:6:115:22 | id | mongoose.js:130:23:130:24 | id | +| mongoose.js:115:11:115:22 | req.query.id | mongoose.js:115:6:115:22 | id | +| mongoose.js:115:11:115:22 | req.query.id | mongoose.js:115:6:115:22 | id | +| mongoose.js:115:25:115:45 | cond | mongoose.js:116:22:116:25 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:116:22:116:25 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:117:21:117:24 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:117:21:117:24 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:118:21:118:24 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:118:21:118:24 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:119:18:119:21 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:119:18:119:21 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:120:22:120:25 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:120:22:120:25 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:121:16:121:19 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:121:16:121:19 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:122:19:122:22 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:122:19:122:22 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:124:28:124:31 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:124:28:124:31 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:125:28:125:31 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:125:28:125:31 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:126:28:126:31 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:126:28:126:31 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:127:18:127:21 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:127:18:127:21 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:128:22:128:25 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:128:22:128:25 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:129:21:129:24 | cond | +| mongoose.js:115:25:115:45 | cond | mongoose.js:129:21:129:24 | cond | +| mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:115:25:115:45 | cond | +| mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:115:25:115:45 | cond | +| mongoose.js:130:23:130:24 | id | mongoose.js:130:16:130:26 | { _id: id } | +| mongoose.js:130:23:130:24 | id | mongoose.js:130:16:130:26 | { _id: id } | | mongooseJsonParse.js:19:11:19:20 | query | mongooseJsonParse.js:23:19:23:23 | query | | mongooseJsonParse.js:19:11:19:20 | query | mongooseJsonParse.js:23:19:23:23 | query | | mongooseJsonParse.js:19:19:19:20 | {} | mongooseJsonParse.js:19:11:19:20 | query | @@ -551,6 +629,22 @@ edges | mongoose.js:94:51:94:55 | query | mongoose.js:21:19:21:26 | req.body | mongoose.js:94:51:94:55 | query | This query depends on $@. | mongoose.js:21:19:21:26 | req.body | a user-provided value | | mongoose.js:96:46:96:50 | query | mongoose.js:21:19:21:26 | req.body | mongoose.js:96:46:96:50 | query | This query depends on $@. | mongoose.js:21:19:21:26 | req.body | a user-provided value | | mongoose.js:111:14:111:18 | query | mongoose.js:21:19:21:26 | req.body | mongoose.js:111:14:111:18 | query | This query depends on $@. | mongoose.js:21:19:21:26 | req.body | a user-provided value | +| mongoose.js:113:31:113:35 | query | mongoose.js:21:19:21:26 | req.body | mongoose.js:113:31:113:35 | query | This query depends on $@. | mongoose.js:21:19:21:26 | req.body | a user-provided value | +| mongoose.js:116:22:116:25 | cond | mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:116:22:116:25 | cond | This query depends on $@. | mongoose.js:115:32:115:45 | req.query.cond | a user-provided value | +| mongoose.js:117:21:117:24 | cond | mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:117:21:117:24 | cond | This query depends on $@. | mongoose.js:115:32:115:45 | req.query.cond | a user-provided value | +| mongoose.js:118:21:118:24 | cond | mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:118:21:118:24 | cond | This query depends on $@. | mongoose.js:115:32:115:45 | req.query.cond | a user-provided value | +| mongoose.js:119:18:119:21 | cond | mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:119:18:119:21 | cond | This query depends on $@. | mongoose.js:115:32:115:45 | req.query.cond | a user-provided value | +| mongoose.js:120:22:120:25 | cond | mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:120:22:120:25 | cond | This query depends on $@. | mongoose.js:115:32:115:45 | req.query.cond | a user-provided value | +| mongoose.js:121:16:121:19 | cond | mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:121:16:121:19 | cond | This query depends on $@. | mongoose.js:115:32:115:45 | req.query.cond | a user-provided value | +| mongoose.js:122:19:122:22 | cond | mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:122:19:122:22 | cond | This query depends on $@. | mongoose.js:115:32:115:45 | req.query.cond | a user-provided value | +| mongoose.js:123:20:123:21 | id | mongoose.js:115:11:115:22 | req.query.id | mongoose.js:123:20:123:21 | id | This query depends on $@. | mongoose.js:115:11:115:22 | req.query.id | a user-provided value | +| mongoose.js:124:28:124:31 | cond | mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:124:28:124:31 | cond | This query depends on $@. | mongoose.js:115:32:115:45 | req.query.cond | a user-provided value | +| mongoose.js:125:28:125:31 | cond | mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:125:28:125:31 | cond | This query depends on $@. | mongoose.js:115:32:115:45 | req.query.cond | a user-provided value | +| mongoose.js:126:28:126:31 | cond | mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:126:28:126:31 | cond | This query depends on $@. | mongoose.js:115:32:115:45 | req.query.cond | a user-provided value | +| mongoose.js:127:18:127:21 | cond | mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:127:18:127:21 | cond | This query depends on $@. | mongoose.js:115:32:115:45 | req.query.cond | a user-provided value | +| mongoose.js:128:22:128:25 | cond | mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:128:22:128:25 | cond | This query depends on $@. | mongoose.js:115:32:115:45 | req.query.cond | a user-provided value | +| mongoose.js:129:21:129:24 | cond | mongoose.js:115:32:115:45 | req.query.cond | mongoose.js:129:21:129:24 | cond | This query depends on $@. | mongoose.js:115:32:115:45 | req.query.cond | a user-provided value | +| mongoose.js:130:16:130:26 | { _id: id } | mongoose.js:115:11:115:22 | req.query.id | mongoose.js:130:16:130:26 | { _id: id } | This query depends on $@. | mongoose.js:115:11:115:22 | req.query.id | a user-provided value | | mongooseJsonParse.js:23:19:23:23 | query | mongooseJsonParse.js:20:30:20:43 | req.query.data | mongooseJsonParse.js:23:19:23:23 | query | This query depends on $@. | mongooseJsonParse.js:20:30:20:43 | req.query.data | a user-provided value | | mongooseModelClient.js:11:16:11:24 | { id: v } | mongooseModelClient.js:10:22:10:29 | req.body | mongooseModelClient.js:11:16:11:24 | { id: v } | This query depends on $@. | mongooseModelClient.js:10:22:10:29 | req.body | a user-provided value | | mongooseModelClient.js:12:16:12:34 | { id: req.body.id } | mongooseModelClient.js:12:22:12:29 | req.body | mongooseModelClient.js:12:16:12:34 | { id: req.body.id } | This query depends on $@. | mongooseModelClient.js:12:22:12:29 | req.body | a user-provided value | diff --git a/javascript/ql/test/query-tests/Security/CWE-089/untyped/mongoose.js b/javascript/ql/test/query-tests/Security/CWE-089/untyped/mongoose.js index 3818b5e05f9..baeb13b1fbc 100644 --- a/javascript/ql/test/query-tests/Security/CWE-089/untyped/mongoose.js +++ b/javascript/ql/test/query-tests/Security/CWE-089/untyped/mongoose.js @@ -109,4 +109,24 @@ app.post('/documents/find', (req, res) => { var C = getQueryConstructor(); new C(X, Y, query); // NOT OK + + Document.findOneAndUpdate(X, query, function () { }); // NOT OK + + let id = req.query.id, cond = req.query.cond; + Document.deleteMany(cond); // NOT OK + Document.deleteOne(cond); // NOT OK + Document.geoSearch(cond); // NOT OK + Document.remove(cond); // NOT OK + Document.replaceOne(cond, Y); // NOT OK + Document.find(cond); // NOT OK + Document.findOne(cond); // NOT OK + Document.findById(id); // NOT OK + Document.findOneAndDelete(cond); // NOT OK + Document.findOneAndRemove(cond); // NOT OK + Document.findOneAndUpdate(cond, Y); // NOT OK + Document.update(cond, Y); // NOT OK + Document.updateMany(cond, Y); // NOT OK + Document.updateOne(cond, Y); // NOT OK + Document.find({ _id: id }); // NOT OK + Document.find({ _id: { $eq: id } }); // OK });