Skip to content

Commit 41b9b2a

Browse files
Python: model MongoDB collection reads as database sources
1 parent 4af6ba6 commit 41b9b2a

11 files changed

Lines changed: 198 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* MongoDB collection reads modeled by the `pymongo` and `flask_pymongo` libraries are now sources under the `database` threat model. This allows queries such as `py/sql-injection` to detect second-order injection through stored documents when the `database` or `local` threat model is enabled.

‎python/ql/lib/semmle/python/frameworks/PyMongo.qll‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,15 @@ private module PyMongo {
128128
override predicate vulnerableToStrings() { none() }
129129
}
130130

131+
/** A call that reads documents from a MongoDB collection. */
132+
private class MongoCollectionRead extends ThreatModelSource::Range {
133+
MongoCollectionRead() { this instanceof MongoCollectionCall }
134+
135+
override string getThreatModel() { result = "database" }
136+
137+
override string getSourceType() { result = "MongoDB collection read" }
138+
}
139+
131140
/**
132141
* See https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.aggregate
133142
*/

‎python/ql/test/library-tests/frameworks/pymongo/ConceptsTest.expected‎

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import python
2+
import experimental.meta.ConceptsTest

‎python/ql/test/library-tests/frameworks/pymongo/DefaultSources.expected‎

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import python
2+
import semmle.python.Concepts
3+
4+
from ActiveThreatModelSource source
5+
where source.getThreatModel() = "database"
6+
select source
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from pymongo import MongoClient
2+
from pymongo.mongo_client import MongoClient as AliasedMongoClient
3+
from flask_pymongo import PyMongo
4+
5+
client = MongoClient()
6+
collection = client.db.users
7+
8+
collection.find({}) # $ threatModelSource[database]=collection.find(..)
9+
collection.find_raw_batches({}) # $ threatModelSource[database]=collection.find_raw_batches(..)
10+
collection.find_one({}) # $ threatModelSource[database]=collection.find_one(..)
11+
collection.find_one_and_delete({}) # $ threatModelSource[database]=collection.find_one_and_delete(..)
12+
collection.find_one_and_replace({}, {}) # $ threatModelSource[database]=collection.find_one_and_replace(..)
13+
collection.find_one_and_update({}, {}) # $ threatModelSource[database]=collection.find_one_and_update(..)
14+
collection.find_and_modify({}) # $ threatModelSource[database]=collection.find_and_modify(..)
15+
16+
# Collection access through subscripts and accessor methods.
17+
client['db']['users'].find_one({}) # $ threatModelSource[database]=client['db']['users'].find_one(..)
18+
client.get_database('db').get_collection('users').find_one({}) # $ threatModelSource[database]=client.get_database(..).get_collection(..).find_one(..)
19+
client.get_default_database().users.find_one({}) # $ threatModelSource[database]=client.get_default_database().users.find_one(..)
20+
AliasedMongoClient().db.users.find_one({}) # $ threatModelSource[database]=AliasedMongoClient().db.users.find_one(..)
21+
22+
# Flask-PyMongo exposes the same collection APIs and find_one_or_404.
23+
mongo = PyMongo()
24+
mongo.db.users.find_one({}) # $ threatModelSource[database]=mongo.db.users.find_one(..)
25+
mongo.db.users.find_one_or_404({}) # $ threatModelSource[database]=mongo.db.users.find_one_or_404(..)
26+
27+
# Write results and counts are not document reads.
28+
collection.insert_one({'user': 'alice'})
29+
collection.delete_one({})
30+
collection.count_documents({})
31+
32+
33+
class UnrelatedCollection:
34+
def find_one(self, query):
35+
return {'user': 'alice'}
36+
37+
38+
# A method name alone does not make a database source.
39+
UnrelatedCollection().find_one({})
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
edges
2+
| pymongo_reads.py:7:5:7:10 | ControlFlowNode for record | pymongo_reads.py:9:20:9:78 | ControlFlowNode for BinaryExpr | provenance | |
3+
| pymongo_reads.py:7:14:7:48 | ControlFlowNode for Attribute() | pymongo_reads.py:7:5:7:10 | ControlFlowNode for record | provenance | |
4+
| pymongo_reads.py:16:9:16:14 | ControlFlowNode for record | pymongo_reads.py:17:24:17:82 | ControlFlowNode for BinaryExpr | provenance | |
5+
| pymongo_reads.py:16:19:16:37 | ControlFlowNode for Attribute() | pymongo_reads.py:16:9:16:14 | ControlFlowNode for record | provenance | |
6+
| pymongo_reads.py:25:9:25:14 | ControlFlowNode for record | pymongo_reads.py:26:22:26:35 | ControlFlowNode for Subscript | provenance | |
7+
| pymongo_reads.py:25:19:25:80 | ControlFlowNode for Attribute() | pymongo_reads.py:25:9:25:14 | ControlFlowNode for record | provenance | |
8+
| pymongo_reads.py:26:9:26:13 | [post] ControlFlowNode for users [List element] | pymongo_reads.py:27:29:27:33 | ControlFlowNode for users [List element] | provenance | |
9+
| pymongo_reads.py:26:22:26:35 | ControlFlowNode for Subscript | pymongo_reads.py:26:9:26:13 | [post] ControlFlowNode for users [List element] | provenance | list.append |
10+
| pymongo_reads.py:27:5:27:13 | ControlFlowNode for in_clause | pymongo_reads.py:28:5:28:7 | ControlFlowNode for sql | provenance | |
11+
| pymongo_reads.py:27:17:27:34 | ControlFlowNode for Attribute() | pymongo_reads.py:27:5:27:13 | ControlFlowNode for in_clause | provenance | |
12+
| pymongo_reads.py:27:29:27:33 | ControlFlowNode for users [List element] | pymongo_reads.py:27:17:27:34 | ControlFlowNode for Attribute() | provenance | str.join |
13+
| pymongo_reads.py:28:5:28:7 | ControlFlowNode for sql | pymongo_reads.py:30:20:30:22 | ControlFlowNode for sql | provenance | |
14+
| pymongo_reads.py:35:5:35:10 | ControlFlowNode for record | pymongo_reads.py:36:41:36:66 | ControlFlowNode for BinaryExpr | provenance | |
15+
| pymongo_reads.py:35:14:35:59 | ControlFlowNode for Attribute() | pymongo_reads.py:35:5:35:10 | ControlFlowNode for record | provenance | |
16+
| pymongo_reads.py:40:5:40:10 | ControlFlowNode for record | pymongo_reads.py:41:41:41:66 | ControlFlowNode for BinaryExpr | provenance | |
17+
| pymongo_reads.py:40:14:40:79 | ControlFlowNode for Attribute() | pymongo_reads.py:40:5:40:10 | ControlFlowNode for record | provenance | |
18+
| pymongo_reads.py:45:5:45:10 | ControlFlowNode for record | pymongo_reads.py:46:41:46:66 | ControlFlowNode for BinaryExpr | provenance | |
19+
| pymongo_reads.py:45:14:45:88 | ControlFlowNode for Attribute() | pymongo_reads.py:45:5:45:10 | ControlFlowNode for record | provenance | |
20+
| pymongo_reads.py:51:5:51:10 | ControlFlowNode for record | pymongo_reads.py:52:41:52:66 | ControlFlowNode for BinaryExpr | provenance | |
21+
| pymongo_reads.py:51:14:51:36 | ControlFlowNode for Attribute() | pymongo_reads.py:51:5:51:10 | ControlFlowNode for record | provenance | |
22+
| pymongo_reads.py:56:5:56:10 | ControlFlowNode for record | pymongo_reads.py:57:41:57:66 | ControlFlowNode for BinaryExpr | provenance | |
23+
| pymongo_reads.py:56:14:56:51 | ControlFlowNode for Attribute() | pymongo_reads.py:56:5:56:10 | ControlFlowNode for record | provenance | |
24+
nodes
25+
| pymongo_reads.py:7:5:7:10 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
26+
| pymongo_reads.py:7:14:7:48 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
27+
| pymongo_reads.py:9:20:9:78 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
28+
| pymongo_reads.py:16:9:16:14 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
29+
| pymongo_reads.py:16:19:16:37 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
30+
| pymongo_reads.py:17:24:17:82 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
31+
| pymongo_reads.py:25:9:25:14 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
32+
| pymongo_reads.py:25:19:25:80 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
33+
| pymongo_reads.py:26:9:26:13 | [post] ControlFlowNode for users [List element] | semmle.label | [post] ControlFlowNode for users [List element] |
34+
| pymongo_reads.py:26:22:26:35 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
35+
| pymongo_reads.py:27:5:27:13 | ControlFlowNode for in_clause | semmle.label | ControlFlowNode for in_clause |
36+
| pymongo_reads.py:27:17:27:34 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
37+
| pymongo_reads.py:27:29:27:33 | ControlFlowNode for users [List element] | semmle.label | ControlFlowNode for users [List element] |
38+
| pymongo_reads.py:28:5:28:7 | ControlFlowNode for sql | semmle.label | ControlFlowNode for sql |
39+
| pymongo_reads.py:30:20:30:22 | ControlFlowNode for sql | semmle.label | ControlFlowNode for sql |
40+
| pymongo_reads.py:35:5:35:10 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
41+
| pymongo_reads.py:35:14:35:59 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
42+
| pymongo_reads.py:36:41:36:66 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
43+
| pymongo_reads.py:40:5:40:10 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
44+
| pymongo_reads.py:40:14:40:79 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
45+
| pymongo_reads.py:41:41:41:66 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
46+
| pymongo_reads.py:45:5:45:10 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
47+
| pymongo_reads.py:45:14:45:88 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
48+
| pymongo_reads.py:46:41:46:66 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
49+
| pymongo_reads.py:51:5:51:10 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
50+
| pymongo_reads.py:51:14:51:36 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
51+
| pymongo_reads.py:52:41:52:66 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
52+
| pymongo_reads.py:56:5:56:10 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
53+
| pymongo_reads.py:56:14:56:51 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
54+
| pymongo_reads.py:57:41:57:66 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
55+
subpaths
56+
#select
57+
| pymongo_reads.py:9:20:9:78 | ControlFlowNode for BinaryExpr | pymongo_reads.py:7:14:7:48 | ControlFlowNode for Attribute() | pymongo_reads.py:9:20:9:78 | ControlFlowNode for BinaryExpr | This SQL query depends on a $@. | pymongo_reads.py:7:14:7:48 | ControlFlowNode for Attribute() | user-provided value |
58+
| pymongo_reads.py:17:24:17:82 | ControlFlowNode for BinaryExpr | pymongo_reads.py:16:19:16:37 | ControlFlowNode for Attribute() | pymongo_reads.py:17:24:17:82 | ControlFlowNode for BinaryExpr | This SQL query depends on a $@. | pymongo_reads.py:16:19:16:37 | ControlFlowNode for Attribute() | user-provided value |
59+
| pymongo_reads.py:30:20:30:22 | ControlFlowNode for sql | pymongo_reads.py:25:19:25:80 | ControlFlowNode for Attribute() | pymongo_reads.py:30:20:30:22 | ControlFlowNode for sql | This SQL query depends on a $@. | pymongo_reads.py:25:19:25:80 | ControlFlowNode for Attribute() | user-provided value |
60+
| pymongo_reads.py:36:41:36:66 | ControlFlowNode for BinaryExpr | pymongo_reads.py:35:14:35:59 | ControlFlowNode for Attribute() | pymongo_reads.py:36:41:36:66 | ControlFlowNode for BinaryExpr | This SQL query depends on a $@. | pymongo_reads.py:35:14:35:59 | ControlFlowNode for Attribute() | user-provided value |
61+
| pymongo_reads.py:41:41:41:66 | ControlFlowNode for BinaryExpr | pymongo_reads.py:40:14:40:79 | ControlFlowNode for Attribute() | pymongo_reads.py:41:41:41:66 | ControlFlowNode for BinaryExpr | This SQL query depends on a $@. | pymongo_reads.py:40:14:40:79 | ControlFlowNode for Attribute() | user-provided value |
62+
| pymongo_reads.py:46:41:46:66 | ControlFlowNode for BinaryExpr | pymongo_reads.py:45:14:45:88 | ControlFlowNode for Attribute() | pymongo_reads.py:46:41:46:66 | ControlFlowNode for BinaryExpr | This SQL query depends on a $@. | pymongo_reads.py:45:14:45:88 | ControlFlowNode for Attribute() | user-provided value |
63+
| pymongo_reads.py:52:41:52:66 | ControlFlowNode for BinaryExpr | pymongo_reads.py:51:14:51:36 | ControlFlowNode for Attribute() | pymongo_reads.py:52:41:52:66 | ControlFlowNode for BinaryExpr | This SQL query depends on a $@. | pymongo_reads.py:51:14:51:36 | ControlFlowNode for Attribute() | user-provided value |
64+
| pymongo_reads.py:57:41:57:66 | ControlFlowNode for BinaryExpr | pymongo_reads.py:56:14:56:51 | ControlFlowNode for Attribute() | pymongo_reads.py:57:41:57:66 | ControlFlowNode for BinaryExpr | This SQL query depends on a $@. | pymongo_reads.py:56:14:56:51 | ControlFlowNode for Attribute() | user-provided value |
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/threat-models
4+
extensible: threatModelConfiguration
5+
data:
6+
- ["database", true, 0]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security/CWE-089/SqlInjection.ql

0 commit comments

Comments
 (0)