{"title":"NoSQL Injection via Unvalidated MongoDB Query Operators","language":"Python","severity":"High","cwe":"CWE-943","source_lines":[10,11],"flow_lines":[10,11,12,13],"sink_lines":[13],"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_query = {'device_id': device_id, 'timestamp': time_range}\n    results = db.telemetry.find(telemetry_query)\n    return jsonify([doc for doc in results])","explanation":"The application accepts user-controlled JSON input for 'time_range' and directly embeds it into a MongoDB query without validation. An attacker can inject MongoDB query operators (like $ne, $gt, $where, or $regex) through the time_range parameter, allowing unauthorized data access or query manipulation.","remediation":"The fix validates and sanitizes both the device_id (ensuring it's a plain string) and the time_range parameter (whitelisting only safe MongoDB comparison operators like $gt, $gte, $lt, $lte and ensuring their values are numeric timestamps or valid ISO datetime strings). This prevents injection of dangerous operators like $ne, $where, $regex, or nested objects that could manipulate the query logic.","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 sanitize_time_range(time_range):\n    \"\"\"Validate and sanitize time_range to only allow safe timestamp values.\"\"\"\n    if not isinstance(time_range, dict):\n        return None\n    \n    sanitized = {}\n    allowed_operators = {'$gt', '$gte', '$lt', '$lte'}\n    \n    for key, value in time_range.items():\n        if key not in allowed_operators:\n            continue\n        if isinstance(value, (int, float)):\n            sanitized[key] = value\n        elif isinstance(value, str):\n            try:\n                parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))\n                sanitized[key] = value\n            except (ValueError, AttributeError):\n                continue\n    \n    return sanitized if sanitized else None\n\ndef sanitize_device_id(device_id):\n    \"\"\"Ensure device_id is a plain string, not an operator object.\"\"\"\n    if not isinstance(device_id, str):\n        return None\n    return device_id\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 = sanitize_device_id(request.json.get('device_id'))\n    if not device_id:\n        return jsonify({'error': 'Invalid device_id: must be a non-empty string'}), 400\n    \n    time_range_input = request.json.get('time_range', {})\n    \n    telemetry_query = {'device_id': device_id}\n    \n    if time_range_input:\n        sanitized_time_range = sanitize_time_range(time_range_input)\n        if sanitized_time_range is None:\n            return jsonify({'error': 'Invalid time_range: must contain valid timestamp operators ($gt, $gte, $lt, $lte) with numeric or ISO datetime string values'}), 400\n        telemetry_query['timestamp'] = sanitized_time_range\n    \n    results = db.telemetry.find(telemetry_query, {'_id': 0})\n    return jsonify([doc for doc in results])"}