Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c0b3245

Browse files
committed
Python: Enrich the NoSql concept
This allows us to make more precise modelling The query tests now pass. I do wonder, if there is a cleaner approach, similar to `TaintedObject` in JavaScript. I want the option to get this query in the hands of the custumors before such an investigation, though.
1 parent 114984b commit c0b3245

5 files changed

Lines changed: 80 additions & 12 deletions

File tree

python/ql/lib/semmle/python/Concepts.qll

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,12 @@ module NoSqlQuery {
389389
abstract class Range extends DataFlow::Node {
390390
/** Gets the argument that specifies the NoSql query to be executed. */
391391
abstract DataFlow::Node getQuery();
392+
393+
/** Holds if this query will unpack/interpret a dictionary */
394+
abstract predicate interpretsDict();
395+
396+
/** Holds if this query can be dangerous when run on a user-controlled string */
397+
abstract predicate vulnerableToStrings();
392398
}
393399
}
394400

@@ -401,6 +407,12 @@ module NoSqlQuery {
401407
class NoSqlQuery extends DataFlow::Node instanceof NoSqlQuery::Range {
402408
/** Gets the argument that specifies the NoSql query to be executed. */
403409
DataFlow::Node getQuery() { result = super.getQuery() }
410+
411+
/** Holds if this query will unpack/interpret a dictionary */
412+
predicate interpretsDict() { super.interpretsDict() }
413+
414+
/** Holds if this query can be dangerous when run on a user-controlled string */
415+
predicate vulnerableToStrings() { super.vulnerableToStrings() }
404416
}
405417

406418
/** Provides classes for modeling NoSql sanitization-related APIs. */

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

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,17 @@ private module NoSql {
9191
result = mongoDBInstance().getMember(["get_collection", "create_collection"]).getReturn()
9292
}
9393

94-
/** This class represents names of find_* relevant `Mongo` collection-level operation methods. */
95-
private class MongoCollectionMethodNames extends string {
96-
MongoCollectionMethodNames() {
97-
this in [
98-
"find", "find_raw_batches", "find_one", "find_one_and_delete", "find_and_modify",
99-
"find_one_and_replace", "find_one_and_update", "find_one_or_404"
100-
]
101-
}
94+
/** Gets the name of a find_* relevant `Mongo` collection-level operation method. */
95+
private string mongoCollectionMethodName() {
96+
result in [
97+
"find", "find_raw_batches", "find_one", "find_one_and_delete", "find_and_modify",
98+
"find_one_and_replace", "find_one_and_update", "find_one_or_404"
99+
]
102100
}
103101

102+
/** Gets the name of a mongo query operator that will interpret JavaScript. */
103+
private string mongoQueryOperator() { result in ["$where", "$function"] }
104+
104105
/**
105106
* Gets a reference to a `Mongo` collection method call
106107
*
@@ -114,10 +115,29 @@ private module NoSql {
114115
*/
115116
private class MongoCollectionCall extends DataFlow::CallCfgNode, NoSqlQuery::Range {
116117
MongoCollectionCall() {
117-
this = mongoCollection().getMember(any(MongoCollectionMethodNames m)).getACall()
118+
this = mongoCollection().getMember(mongoCollectionMethodName()).getACall()
118119
}
119120

120121
override DataFlow::Node getQuery() { result = this.getArg(0) }
122+
123+
override predicate interpretsDict() { any() }
124+
125+
override predicate vulnerableToStrings() { none() }
126+
}
127+
128+
private class MongoCollectionQueryOperator extends API::CallNode, NoSqlQuery::Range {
129+
DataFlow::Node query;
130+
131+
MongoCollectionQueryOperator() {
132+
this = mongoCollection().getMember(mongoCollectionMethodName()).getACall() and
133+
query = this.getParameter(0).getSubscript(mongoQueryOperator()).asSink()
134+
}
135+
136+
override DataFlow::Node getQuery() { result = query }
137+
138+
override predicate interpretsDict() { none() }
139+
140+
override predicate vulnerableToStrings() { any() }
121141
}
122142

123143
/**
@@ -146,6 +166,10 @@ private module NoSql {
146166
}
147167

148168
override DataFlow::Node getQuery() { result = this.getArgByName(_) }
169+
170+
override predicate interpretsDict() { any() }
171+
172+
override predicate vulnerableToStrings() { none() }
149173
}
150174

151175
/** Gets a reference to `mongosanitizer.sanitizer.sanitize` */
@@ -176,4 +200,23 @@ private module NoSql {
176200

177201
override DataFlow::Node getAnInput() { result = this.getArg(0) }
178202
}
203+
204+
/**
205+
* An equality operator can protect against dictionary interpretation.
206+
* For instance, in `{'password': {"$eq": password} }`, if a dictionary is injected into
207+
* `password`, it will not match.
208+
*/
209+
private class EqualityOperator extends DataFlow::Node, NoSqlSanitizer::Range {
210+
EqualityOperator() {
211+
this =
212+
mongoCollection()
213+
.getMember(mongoCollectionMethodName())
214+
.getParameter(0)
215+
.getASubscript*()
216+
.getSubscript("$eq")
217+
.asSink()
218+
}
219+
220+
override DataFlow::Node getAnInput() { result = this }
221+
}
179222
}

python/ql/lib/semmle/python/security/dataflow/NoSQLInjectionCustomizations.qll

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ module NoSqlInjection {
3636

3737
class RemoteFlowSourceAsStringSource extends RemoteFlowSource, StringSource { }
3838

39+
class NoSqlQueryAsStringSink extends StringSink {
40+
NoSqlQueryAsStringSink() {
41+
exists(NoSqlQuery noSqlQuery | this = noSqlQuery.getQuery() |
42+
noSqlQuery.vulnerableToStrings()
43+
)
44+
}
45+
}
46+
3947
class NoSqlQueryAsDictSink extends DictSink {
4048
NoSqlQueryAsDictSink() { this = any(NoSqlQuery noSqlQuery).getQuery() }
4149
}

python/ql/lib/semmle/python/security/dataflow/NoSQLInjectionQuery.qll

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ module Config implements DataFlow::StateConfigSig {
1717

1818
predicate isSink(DataFlow::Node source, FlowState state) {
1919
source instanceof C::StringSink and
20-
state instanceof C::StringInput
20+
(
21+
state instanceof C::StringInput
22+
or
23+
// since dictionaries can encode strings
24+
state instanceof C::DictInput
25+
)
2126
or
2227
source instanceof C::DictSink and
2328
state instanceof C::DictInput

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def as_dict():
3434
def as_dict_hardened():
3535
author_string = request.args['author']
3636
author = json.loads(author_string)
37-
post = posts.find_one({'author': {"$eq": author}}) # $ SPURIOUS: result=BAD
37+
post = posts.find_one({'author': {"$eq": author}}) # $ result=OK
3838
return show_post(post, author)
3939

4040
@app.route('/byWhere', methods=['GET'])
@@ -43,7 +43,7 @@ def by_where():
4343
# Use `" | "a" === "a` as author
4444
# making the query `this.author === "" | "a" === "a"`
4545
# Found by http://127.0.0.1:5000/byWhere?author=%22%20|%20%22a%22%20===%20%22a
46-
post = posts.find_one({'$where': 'this.author === "'+author+'"'}) # $ MISSING: result=BAD
46+
post = posts.find_one({'$where': 'this.author === "'+author+'"'}) # $ result=BAD
4747
return show_post(post, author)
4848

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

0 commit comments

Comments
 (0)