{"title":"MongoDB NoSQL Injection via Unvalidated Query Operators","language":"Python","severity":"High","cwe":"CWE-943","source_lines":[10,11],"flow_lines":[10,11,12],"sink_lines":[12],"vulnerable_code":"from flask import Flask, request, jsonify\nfrom pymongo import MongoClient\n\napp = Flask(__name__)\ndb = MongoClient('mongodb://localhost:27017/').iot_devices\n\n@app.route('/api/device/telemetry', methods=['POST'])\ndef fetch_device_telemetry():\n    device_id = request.json.get('device_id')\n    time_range = request.json.get('time_range', {})\n    telemetry_data = db.telemetry.find({'device_id': device_id, 'timestamp': time_range})\n    return jsonify([doc for doc in telemetry_data])","explanation":"The application accepts unsanitized JSON input for 'time_range' and directly passes it to MongoDB's find() query. An attacker can inject MongoDB query operators (like $gt, $ne, $where) through the time_range parameter to manipulate the query logic, bypass authorization checks, and access unauthorized telemetry data from other devices.","remediation":"The fix validates both the device_id (ensuring it's a plain string and not a query operator) and the time_range parameter by whitelisting only safe MongoDB comparison operators ($gt, $gte, $lt, $lte) and ensuring their values are valid timestamps (numeric or ISO format strings). This prevents attackers from injecting arbitrary MongoDB operators like $ne, $where, or $regex to manipulate query logic and access unauthorized data.","secure_code":"from flask import Flask, request, jsonify\nfrom pymongo import MongoClient\nfrom datetime import datetime\n\napp = Flask(__name__)\ndb = MongoClient('mongodb://localhost:27017/').iot_devices\n\ndef validate_device_id(device_id):\n    \"\"\"Ensure device_id is a plain string, not a query operator.\"\"\"\n    if not isinstance(device_id, str):\n        return None\n    if device_id.startswith('$'):\n        return None\n    return device_id\n\ndef validate_time_range(time_range):\n    \"\"\"Parse and validate time_range into a safe MongoDB query using only whitelisted operators.\"\"\"\n    if time_range is None:\n        return {}\n    \n    if not isinstance(time_range, dict):\n        return None\n    \n    safe_query = {}\n    allowed_operators = {'$gt', '$gte', '$lt', '$lte'}\n    \n    for key, value in time_range.items():\n        if key not in allowed_operators:\n            return None\n        if isinstance(value, (int, float)):\n            safe_query[key] = value\n        elif isinstance(value, str):\n            try:\n                safe_query[key] = datetime.fromisoformat(value)\n            except (ValueError, TypeError):\n                return None\n        else:\n            return None\n    \n    return safe_query\n\n@app.route('/api/device/telemetry', methods=['POST'])\ndef fetch_device_telemetry():\n    if not request.json:\n        return jsonify({'error': 'Invalid request body'}), 400\n    \n    device_id = validate_device_id(request.json.get('device_id'))\n    if device_id is None:\n        return jsonify({'error': 'Invalid device_id parameter'}), 400\n    \n    time_range_input = request.json.get('time_range', {})\n    time_range = validate_time_range(time_range_input)\n    if time_range is None:\n        return jsonify({'error': 'Invalid time_range parameter. Use {\"$gte\": \"ISO_DATE\", \"$lte\": \"ISO_DATE\"} format.'}), 400\n    \n    query = {'device_id': device_id}\n    if time_range:\n        query['timestamp'] = time_range\n    \n    telemetry_data = db.telemetry.find(query, {'_id': 0})\n    return jsonify([doc for doc in telemetry_data])"}