From 114984bd8cb92ba8f06dbb3902eb48d3481a1a67 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Tue, 29 Aug 2023 16:16:38 +0200 Subject: [PATCH] Python: Added tests based on security analysis currently we do not: - recognize the pattern `{'author': {"$eq": author}}` as protected - recognize arguements to `$where` (and friends) as vulnerable --- .../CWE-943-NoSqlInjection/PoC/populate.py | 16 ++++++ .../CWE-943-NoSqlInjection/PoC/readme.md | 19 +++++++ .../PoC/requirements.txt | 2 + .../CWE-943-NoSqlInjection/PoC/server.py | 55 +++++++++++++++++++ .../Security/CWE-943-NoSqlInjection/options | 1 + 5 files changed, 93 insertions(+) create mode 100644 python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/populate.py create mode 100644 python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/readme.md create mode 100644 python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/requirements.txt create mode 100644 python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/server.py create mode 100644 python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/options diff --git a/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/populate.py b/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/populate.py new file mode 100644 index 00000000000..026f72bdfe5 --- /dev/null +++ b/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/populate.py @@ -0,0 +1,16 @@ +from pymongo import MongoClient +client = MongoClient() + +db = client.test_database + +import datetime +post = { + "author": "Mike", + "text": "My first blog post!", + "tags": ["mongodb", "python", "pymongo"], + "date": datetime.datetime.now(tz=datetime.timezone.utc), +} + +posts = db.posts +post_id = posts.insert_one(post).inserted_id +post_id diff --git a/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/readme.md b/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/readme.md new file mode 100644 index 00000000000..78e7e92e48f --- /dev/null +++ b/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/readme.md @@ -0,0 +1,19 @@ +Tutorials: +- [pymongo](https://pymongo.readthedocs.io/en/stable/tutorial.html) +- [install mongodb](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-os-x/#std-label-install-mdb-community-macos) + +I recommend creating a virtual environment with venv and then installing dependencies via +``` +python -m pip --install -r requirements.txt +``` + +Start mongodb: +``` +mongod --config /usr/local/etc/mongod.conf --fork +``` +run flask app: +``` +flask --app server run +``` + +Navigate to the root to see routes. For each route try to get the system to reveal the person in the database. If you know the name, you can just input it, but in some cases you can get to the person without knowing the name! diff --git a/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/requirements.txt b/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/requirements.txt new file mode 100644 index 00000000000..a98715ed1e4 --- /dev/null +++ b/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/requirements.txt @@ -0,0 +1,2 @@ +flask +pymongo diff --git a/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/server.py b/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/server.py new file mode 100644 index 00000000000..59b51963583 --- /dev/null +++ b/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/server.py @@ -0,0 +1,55 @@ +from flask import Flask, request +from pymongo import MongoClient +import json + +client = MongoClient() +db = client.test_database +posts = db.posts + +app = Flask(__name__) + + +def show_post(post, query): + if post: + return "You found " + post['author'] + "!" + else: + return "You did not find " + query + +@app.route('/plain', methods=['GET']) +def plain(): + author = request.args['author'] + post = posts.find_one({'author': author}) # $ result=OK + return show_post(post, author) + +@app.route('/dict', methods=['GET']) +def as_dict(): + author_string = request.args['author'] + author = json.loads(author_string) + # Use {"$ne": 1} as author + # Found by http://127.0.0.1:5000/dict?author={%22$ne%22:1} + post = posts.find_one({'author': author}) # $ result=BAD + return show_post(post, author) + +@app.route('/dictHardened', methods=['GET']) +def as_dict_hardened(): + author_string = request.args['author'] + author = json.loads(author_string) + post = posts.find_one({'author': {"$eq": author}}) # $ SPURIOUS: result=BAD + return show_post(post, author) + +@app.route('/byWhere', methods=['GET']) +def by_where(): + author = request.args['author'] + # Use `" | "a" === "a` as author + # making the query `this.author === "" | "a" === "a"` + # Found by http://127.0.0.1:5000/byWhere?author=%22%20|%20%22a%22%20===%20%22a + post = posts.find_one({'$where': 'this.author === "'+author+'"'}) # $ MISSING: result=BAD + return show_post(post, author) + +@app.route('/', methods=['GET']) +def show_routes(): + links = [] + for rule in app.url_map.iter_rules(): + if "GET" in rule.methods: + links.append((rule.rule, rule.endpoint)) + return links diff --git a/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/options b/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/options new file mode 100644 index 00000000000..48ecb907736 --- /dev/null +++ b/python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/options @@ -0,0 +1 @@ +semmle-extractor-options: --max-import-depth=1 -r PoC