diff --git a/rules/python/lang/nosql_injection.yml b/rules/python/lang/nosql_injection.yml new file mode 100644 index 00000000..ab6bd905 --- /dev/null +++ b/rules/python/lang/nosql_injection.yml @@ -0,0 +1,172 @@ +imports: + - python_shared_common_user_input + - python_shared_lang_import1 + - python_shared_lang_import2 +patterns: + - pattern: | + $.$($<...>$$<...>) + filters: + - variable: MONGODB_COLLECTION + detection: python_lang_nosql_injection_mongodb_collection + scope: cursor + - variable: METHOD + values: + - bulk_write + - insert_one + - insert_many + - replace_one + - update_one + - update_many + - delete_one + - delete_many + - find + - find_one + - find_one_and_delete + - find_one_and_replace + - find_one_and_update + - variable: USER_INPUT + detection: python_lang_nosql_injection_user_input + scope: result +auxiliary: + - id: python_lang_nosql_injection_mongodb_collection + patterns: + - pattern: $[$] + filters: + - variable: MONGO_DB + detection: python_lang_nosql_injection_mongodb_database + scope: result + - variable: STR + string_regex: \A.*\z + - pattern: $.$ + filters: + - variable: MONGO_DB + detection: python_lang_nosql_injection_mongodb_database + scope: result + - not: + variable: COLLECTION + values: + - codec_options + - read_preference + - write_concern + - read_concern + - client + - name + - id: python_lang_nosql_injection_mongodb_database + patterns: + - pattern: $[$] + filters: + - variable: MONGO_CLIENT + detection: python_lang_nosql_injection_mongodb_client + scope: result + - variable: STR + string_regex: \A.*\z + - pattern: $.$ + filters: + - variable: MONGO_CLIENT + detection: python_lang_nosql_injection_mongodb_client + scope: cursor + - not: + variable: DB + values: + - topology_description + - address + - primary + - secondaries + - arbiters + - is_primary + - is_mongos + - nodes + - codec_options + - read_preference + - write_concern + - read_concern + - options + - id: python_lang_nosql_injection_mongodb_client + patterns: + - pattern: $($) + filters: + - variable: STR + string_regex: \A.*\z + - variable: MONGO_CLIENT + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [pymongo] + - variable: NAME + values: [MongoClient] + - id: python_lang_nosql_injection_user_input + sanitizer: python_lang_nosql_injection_sanitizer + patterns: + - pattern: $ + filters: + - variable: USER_INPUT + detection: python_shared_common_user_input + scope: cursor + - id: python_lang_nosql_injection_sanitizer + patterns: + - str($$<_>) + - pattern: $($$<_>) + filters: + - variable: BSON_TYPE + detection: python_shared_lang_import1 + scope: cursor + filters: + - variable: MODULE1 + values: [bson] + - variable: NAME + values: [ObjectId] + - pattern: $($$<_>) + filters: + - variable: BSON_TYPE + detection: python_shared_lang_import2 + scope: cursor + filters: + - variable: MODULE1 + values: [bson] + - variable: MODULE2 + values: + - int64 + - decimal128 + - datetime_ms + - variable: NAME + values: + - Int64 + - Decimal128 + - DatetimeMS +languages: + - python +severity: critical +metadata: + description: Unsanitized input in NoSQL query + remediation_message: | + ## Description + + Using unsanitized data in NoSQL queries exposes your application to NoSQL injection attacks. This vulnerability arises when user input, request data, or any externally influenced data is directly passed into a NoSQL query function without proper sanitization. + + ## Remediations + + - **Do not** include raw, unsanitized user input in NoSQL queries. This practice can lead to NoSQL injection vulnerabilities. + ```python + query = '{ "username": "' + unsafe_input + '" }' + collection.findOne(query) # unsafe + ``` + - **Do** use parameterized queries instead of concatenating strings. This ensures that you take advantage of any built-in input sanitization that your NoSQL client may offer. + ```python + collection.findOne({ "username": unsafe_input }); + ``` + - **Do** sanitize and validate all input data before using it in NoSQL queries. Ensuring data is properly sanitized and validated can prevent NoSQL injection attacks. For example, you could parse external data as a string or convert the data into an appropriate BSON type. + ```python + username = request.GET.get("username") + collection.findOne({ "username": str(unsafe_input) }); + # or + collection.findOne({ "uuid": bson.ObjectId(unsafe_input) }) + ``` + + ## References + + - [OWASP NoSQL injection explained](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/05.6-Testing_for_NoSQL_Injection) + cwe_id: + - 943 + id: python_lang_nosql_injection + documentation_url: https://docs.bearer.com/reference/rules/python_lang_nosql_injection diff --git a/tests/python/lang/nosql_injection/test.js b/tests/python/lang/nosql_injection/test.js new file mode 100644 index 00000000..eb439355 --- /dev/null +++ b/tests/python/lang/nosql_injection/test.js @@ -0,0 +1,20 @@ +const { + createNewInvoker, + getEnvironment, +} = require("../../../helper.js") +const { ruleId, ruleFile, testBase } = getEnvironment(__dirname) + +describe(ruleId, () => { + const invoke = createNewInvoker(ruleId, ruleFile, testBase) + + test("nosql_injection", () => { + const testCase = "main.py" + + const results = invoke(testCase) + + expect(results).toEqual({ + Missing: [], + Extra: [] + }) + }) +}) \ No newline at end of file diff --git a/tests/python/lang/nosql_injection/testdata/main.py b/tests/python/lang/nosql_injection/testdata/main.py new file mode 100644 index 00000000..312dc5b8 --- /dev/null +++ b/tests/python/lang/nosql_injection/testdata/main.py @@ -0,0 +1,41 @@ +from pymongo import MongoClient + +def bad(request): + user_input = request.GET.get('user_data') + + client = MongoClient('mongodb://localhost:27017/') + db = client['my_db'] + collection = db['customers'] + # bearer:expected python_lang_nosql_injection + collection.find_one({ "username": user_input }) + + collection = db.customers + # bearer:expected python_lang_nosql_injection + collection.find_one({ "username": user_input }) + + query = '{ "username": "' + user_input + '" }' + # bearer:expected python_lang_nosql_injection + customer = collection.find_one(query) + + query = json.loads('{ "username": "' + user_input + '" }') + # bearer:expected python_lang_nosql_injection + customer = collection.find_one(query) + + # bearer:expected python_lang_nosql_injection + collection.find_one( { "cart_total": request.GET.get('total') } ) + + +from bson import ObjectId +from bson.decimal128 import Decimal128 + +def ok(request): + user_input = request.GET.get('username') + + client = MongoClient('mongodb://localhost:27017/') + db = client['my_db'] + collection = db['customers'] + collection.find_one( { "username": str(user_input) } ) + + collection.find_one( { "uuid": ObjectId(user_uuid) } ) + collection.find_one( { "cart_total": Decimal128(request.GET.get('total')) } ) +