Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* 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.
9 changes: 9 additions & 0 deletions python/ql/lib/semmle/python/frameworks/PyMongo.qll
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ private module PyMongo {
override predicate vulnerableToStrings() { none() }
}

/** A call that reads documents from a MongoDB collection. */
private class MongoCollectionRead extends ThreatModelSource::Range {
MongoCollectionRead() { this instanceof MongoCollectionCall }

override string getThreatModel() { result = "database" }

override string getSourceType() { result = "MongoDB collection read" }
}

/**
* See https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.aggregate
*/
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import python
import experimental.meta.ConceptsTest
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import python
import semmle.python.Concepts

from ActiveThreatModelSource source
where source.getThreatModel() = "database"
select source
39 changes: 39 additions & 0 deletions python/ql/test/library-tests/frameworks/pymongo/reads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from pymongo import MongoClient
from pymongo.mongo_client import MongoClient as AliasedMongoClient
from flask_pymongo import PyMongo

client = MongoClient()
collection = client.db.users

collection.find({}) # $ threatModelSource[database]=collection.find(..)
collection.find_raw_batches({}) # $ threatModelSource[database]=collection.find_raw_batches(..)
collection.find_one({}) # $ threatModelSource[database]=collection.find_one(..)
collection.find_one_and_delete({}) # $ threatModelSource[database]=collection.find_one_and_delete(..)
collection.find_one_and_replace({}, {}) # $ threatModelSource[database]=collection.find_one_and_replace(..)
collection.find_one_and_update({}, {}) # $ threatModelSource[database]=collection.find_one_and_update(..)
collection.find_and_modify({}) # $ threatModelSource[database]=collection.find_and_modify(..)

# Collection access through subscripts and accessor methods.
client['db']['users'].find_one({}) # $ threatModelSource[database]=client['db']['users'].find_one(..)
client.get_database('db').get_collection('users').find_one({}) # $ threatModelSource[database]=client.get_database(..).get_collection(..).find_one(..)
client.get_default_database().users.find_one({}) # $ threatModelSource[database]=client.get_default_database().users.find_one(..)
AliasedMongoClient().db.users.find_one({}) # $ threatModelSource[database]=AliasedMongoClient().db.users.find_one(..)

# Flask-PyMongo exposes the same collection APIs and find_one_or_404.
mongo = PyMongo()
mongo.db.users.find_one({}) # $ threatModelSource[database]=mongo.db.users.find_one(..)
mongo.db.users.find_one_or_404({}) # $ threatModelSource[database]=mongo.db.users.find_one_or_404(..)

# Write results and counts are not document reads.
collection.insert_one({'user': 'alice'})
collection.delete_one({})
collection.count_documents({})


class UnrelatedCollection:
def find_one(self, query):
return {'user': 'alice'}


# A method name alone does not make a database source.
UnrelatedCollection().find_one({})
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
edges
| pymongo_reads.py:7:5:7:10 | ControlFlowNode for record | pymongo_reads.py:9:20:9:78 | ControlFlowNode for BinaryExpr | provenance | |
| pymongo_reads.py:7:14:7:48 | ControlFlowNode for Attribute() | pymongo_reads.py:7:5:7:10 | ControlFlowNode for record | provenance | |
| pymongo_reads.py:16:9:16:14 | ControlFlowNode for record | pymongo_reads.py:17:24:17:82 | ControlFlowNode for BinaryExpr | provenance | |
| pymongo_reads.py:16:19:16:37 | ControlFlowNode for Attribute() | pymongo_reads.py:16:9:16:14 | ControlFlowNode for record | provenance | |
| pymongo_reads.py:25:9:25:14 | ControlFlowNode for record | pymongo_reads.py:26:22:26:35 | ControlFlowNode for Subscript | provenance | |
| pymongo_reads.py:25:19:25:80 | ControlFlowNode for Attribute() | pymongo_reads.py:25:9:25:14 | ControlFlowNode for record | provenance | |
| 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 | |
| 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 |
| pymongo_reads.py:27:5:27:13 | ControlFlowNode for in_clause | pymongo_reads.py:28:5:28:7 | ControlFlowNode for sql | provenance | |
| pymongo_reads.py:27:17:27:34 | ControlFlowNode for Attribute() | pymongo_reads.py:27:5:27:13 | ControlFlowNode for in_clause | provenance | |
| 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 |
| pymongo_reads.py:28:5:28:7 | ControlFlowNode for sql | pymongo_reads.py:30:20:30:22 | ControlFlowNode for sql | provenance | |
| pymongo_reads.py:35:5:35:10 | ControlFlowNode for record | pymongo_reads.py:36:41:36:66 | ControlFlowNode for BinaryExpr | provenance | |
| pymongo_reads.py:35:14:35:59 | ControlFlowNode for Attribute() | pymongo_reads.py:35:5:35:10 | ControlFlowNode for record | provenance | |
| pymongo_reads.py:40:5:40:10 | ControlFlowNode for record | pymongo_reads.py:41:41:41:66 | ControlFlowNode for BinaryExpr | provenance | |
| pymongo_reads.py:40:14:40:79 | ControlFlowNode for Attribute() | pymongo_reads.py:40:5:40:10 | ControlFlowNode for record | provenance | |
| pymongo_reads.py:45:5:45:10 | ControlFlowNode for record | pymongo_reads.py:46:41:46:66 | ControlFlowNode for BinaryExpr | provenance | |
| pymongo_reads.py:45:14:45:88 | ControlFlowNode for Attribute() | pymongo_reads.py:45:5:45:10 | ControlFlowNode for record | provenance | |
| pymongo_reads.py:51:5:51:10 | ControlFlowNode for record | pymongo_reads.py:52:41:52:66 | ControlFlowNode for BinaryExpr | provenance | |
| pymongo_reads.py:51:14:51:36 | ControlFlowNode for Attribute() | pymongo_reads.py:51:5:51:10 | ControlFlowNode for record | provenance | |
| pymongo_reads.py:56:5:56:10 | ControlFlowNode for record | pymongo_reads.py:57:41:57:66 | ControlFlowNode for BinaryExpr | provenance | |
| pymongo_reads.py:56:14:56:51 | ControlFlowNode for Attribute() | pymongo_reads.py:56:5:56:10 | ControlFlowNode for record | provenance | |
nodes
| pymongo_reads.py:7:5:7:10 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
| pymongo_reads.py:7:14:7:48 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| pymongo_reads.py:9:20:9:78 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| pymongo_reads.py:16:9:16:14 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
| pymongo_reads.py:16:19:16:37 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| pymongo_reads.py:17:24:17:82 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| pymongo_reads.py:25:9:25:14 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
| pymongo_reads.py:25:19:25:80 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| pymongo_reads.py:26:9:26:13 | [post] ControlFlowNode for users [List element] | semmle.label | [post] ControlFlowNode for users [List element] |
| pymongo_reads.py:26:22:26:35 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
| pymongo_reads.py:27:5:27:13 | ControlFlowNode for in_clause | semmle.label | ControlFlowNode for in_clause |
| pymongo_reads.py:27:17:27:34 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| pymongo_reads.py:27:29:27:33 | ControlFlowNode for users [List element] | semmle.label | ControlFlowNode for users [List element] |
| pymongo_reads.py:28:5:28:7 | ControlFlowNode for sql | semmle.label | ControlFlowNode for sql |
| pymongo_reads.py:30:20:30:22 | ControlFlowNode for sql | semmle.label | ControlFlowNode for sql |
| pymongo_reads.py:35:5:35:10 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
| pymongo_reads.py:35:14:35:59 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| pymongo_reads.py:36:41:36:66 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| pymongo_reads.py:40:5:40:10 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
| pymongo_reads.py:40:14:40:79 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| pymongo_reads.py:41:41:41:66 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| pymongo_reads.py:45:5:45:10 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
| pymongo_reads.py:45:14:45:88 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| pymongo_reads.py:46:41:46:66 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| pymongo_reads.py:51:5:51:10 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
| pymongo_reads.py:51:14:51:36 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| pymongo_reads.py:52:41:52:66 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| pymongo_reads.py:56:5:56:10 | ControlFlowNode for record | semmle.label | ControlFlowNode for record |
| pymongo_reads.py:56:14:56:51 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
| pymongo_reads.py:57:41:57:66 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
subpaths
#select
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
extensions:
- addsTo:
pack: codeql/threat-models
extensible: threatModelConfiguration
data:
- ["database", true, 0]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Security/CWE-089/SqlInjection.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import sqlite3
from pymongo import MongoClient
from flask_pymongo import PyMongo


def find_one():
record = MongoClient().db.users.find_one({})
cursor = sqlite3.connect(':memory:').cursor()
cursor.execute("SELECT * FROM users WHERE name = '" + record['user'] + "'") # $ Alert
cursor.execute('SELECT * FROM users WHERE name = ?', (record['user'],))


def find_iteration():
collection = MongoClient().db.users
cursor = sqlite3.connect(':memory:').cursor()
for record in collection.find({}):
cursor.execute("SELECT * FROM users WHERE name = '" + record['user'] + "'") # $ Alert
cursor.execute('SELECT * FROM users WHERE name = ?', (record['user'],))


def sync_users():
# Regression for the list accumulation and join in issue #21775.
users = []
client = MongoClient('mongodb://localhost:27017/')
for record in client.auth.users.find({'role': 'dev'}, {'user': 1, '_id': 0}):
users.append(record['user'])
in_clause = "', '".join(users)
sql = "SELECT * FROM user_info WHERE username IN ('" + in_clause + "')"
cursor = sqlite3.connect(':memory:').cursor()
cursor.execute(sql) # $ Alert
cursor.execute('SELECT * FROM user_info WHERE username = ?', (in_clause,))


def find_and_delete():
record = MongoClient().db.users.find_one_and_delete({})
sqlite3.connect(':memory:').execute('SELECT ' + record['user']) # $ Alert


def find_and_replace():
record = MongoClient().db.users.find_one_and_replace({}, {'user': 'alice'})
sqlite3.connect(':memory:').execute('SELECT ' + record['user']) # $ Alert


def find_and_update():
record = MongoClient().db.users.find_one_and_update({}, {'$set': {'user': 'alice'}})
sqlite3.connect(':memory:').execute('SELECT ' + record['user']) # $ Alert


def collection_accessors():
collection = MongoClient().get_database('db').get_collection('users')
record = collection.find_one({})
sqlite3.connect(':memory:').execute('SELECT ' + record['user']) # $ Alert


def flask_pymongo():
record = PyMongo().db.users.find_one_or_404({})
sqlite3.connect(':memory:').execute('SELECT ' + record['user']) # $ Alert


class UnrelatedCollection:
def find_one(self, query):
return {'user': 'alice'}


def unrelated_method():
record = UnrelatedCollection().find_one({})
sqlite3.connect(':memory:').execute('SELECT ' + record['user'])