Skip to content

Commit d9f63e1

Browse files
committed
Python: Split modelling of query operators
`$where` and `$function` behave quite differently.
1 parent 154a369 commit d9f63e1

File tree

2 files changed

+29
-11
lines changed
  • python/ql
    • lib/semmle/python/frameworks
    • test/query-tests/Security/CWE-943-NoSqlInjection/PoC

2 files changed

+29
-11
lines changed

python/ql/lib/semmle/python/frameworks/NoSQL.qll

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,6 @@ private module NoSql {
9999
]
100100
}
101101

102-
/** Gets the name of a mongo query operator that will interpret JavaScript. */
103-
private string mongoQueryOperator() { result in ["$where", "$function"] }
104-
105102
/**
106103
* Gets a reference to a `Mongo` collection method call
107104
*
@@ -125,12 +122,34 @@ private module NoSql {
125122
override predicate vulnerableToStrings() { none() }
126123
}
127124

128-
private class MongoCollectionQueryOperator extends API::CallNode, NoSqlQuery::Range {
125+
/** The `$where` query operator executes a string as JavaScript. */
126+
private class WhereQueryOperator extends API::CallNode, NoSqlQuery::Range {
129127
DataFlow::Node query;
130128

131-
MongoCollectionQueryOperator() {
129+
WhereQueryOperator() {
132130
this = mongoCollection().getMember(mongoCollectionMethodName()).getACall() and
133-
query = this.getParameter(0).getSubscript(mongoQueryOperator()).asSink()
131+
query = this.getParameter(0).getSubscript("$where").asSink()
132+
}
133+
134+
override DataFlow::Node getQuery() { result = query }
135+
136+
override predicate interpretsDict() { none() }
137+
138+
override predicate vulnerableToStrings() { any() }
139+
}
140+
141+
/** The `$function` query operator executes its `body` string as JavaScript. */
142+
private class FunctionQueryOperator extends API::CallNode, NoSqlQuery::Range {
143+
DataFlow::Node query;
144+
145+
FunctionQueryOperator() {
146+
this = mongoCollection().getMember(mongoCollectionMethodName()).getACall() and
147+
query =
148+
this.getParameter(0)
149+
.getASubscript*()
150+
.getSubscript("$function")
151+
.getSubscript("body")
152+
.asSink()
134153
}
135154

136155
override DataFlow::Node getQuery() { result = query }

python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/server.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,30 +46,29 @@ def by_where():
4646
post = posts.find_one({'$where': 'this.author === "'+author+'"'}) # $ result=BAD
4747
return show_post(post, author)
4848

49-
5049
@app.route('/byFunction', methods=['GET'])
5150
def by_function():
5251
author = request.args['author']
5352
search = {
54-
"body": 'function(author) { return(author === "'+author+'") }',
53+
"body": 'function(author) { return(author === "'+author+'") }', # $ result=BAD
5554
"args": [ "$author" ],
5655
"lang": "js"
5756
}
5857
# Use `" | "a" === "a` as author
5958
# making the query `this.author === "" | "a" === "a"`
6059
# Found by http://127.0.0.1:5000/byFunction?author=%22%20|%20%22a%22%20===%20%22a
61-
post = posts.find_one({'$expr': {'$function': search}}) # $ MISING: result=BAD
60+
post = posts.find_one({'$expr': {'$function': search}}) # $ result=BAD
6261
return show_post(post, author)
6362

6463
@app.route('/byFunctionArg', methods=['GET'])
6564
def by_function_arg():
6665
author = request.args['author']
6766
search = {
68-
"body": 'function(author, target) { return(author === target) }',
67+
"body": 'function(author, target) { return(author === target) }', # $ result=OK
6968
"args": [ "$author", author ],
7069
"lang": "js"
7170
}
72-
post = posts.find_one({'$expr': {'$function': search}}) # $ result=OK
71+
post = posts.find_one({'$expr': {'$function': search}}) # $ SPURIOUS: result=BAD
7372
return show_post(post, author)
7473

7574
@app.route('/', methods=['GET'])

0 commit comments

Comments
 (0)