{"title":"NoSQL Injection via MongoDB Query Operator Injection","language":"Python","severity":"High","cwe":"CWE-943","source_lines":[9,11],"flow_lines":[9,11,12,15,17],"sink_lines":[17],"vulnerable_code":"from flask import Flask, request, jsonify\nfrom pymongo import MongoClient\n\napp = Flask(__name__)\ndb_client = MongoClient('mongodb://localhost:27017/')\niot_db = db_client['smart_home_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    sensor_type = request.json.get('sensor_type')\n    query_filter = {\n        'device_id': device_id,\n        'timestamp': time_range,\n        'sensor': sensor_type\n    }\n    telemetry_data = list(iot_db.telemetry.find(query_filter))\n    for record in telemetry_data:\n        record['_id'] = str(record['_id'])\n    return jsonify({'status': 'success', 'data': telemetry_data, 'count': len(telemetry_data)})","explanation":"The application accepts untrusted JSON input including 'device_id', 'time_range', and 'sensor_type' parameters and directly embeds them into a MongoDB query filter without validation or sanitization. The 'time_range' parameter is particularly dangerous as it accepts a raw dictionary, allowing attackers to inject MongoDB query operators like $ne, $gt, $where, or $regex to bypass intended query logic, access unauthorized data, or cause denial of service.","remediation":"The fix validates all user inputs by ensuring 'device_id' and 'sensor_type' are plain strings (rejecting dictionaries that could contain MongoDB operators), and by parsing 'time_range' through a strict sanitization function that only constructs safe $gte/$lte queries from validated ISO date strings or numeric timestamps. This prevents attackers from injecting arbitrary MongoDB query operators like $ne, $where, or $regex into any of the query parameters.","secure_code":"from flask import Flask, request, jsonify\nfrom pymongo import MongoClient\nfrom datetime import datetime\n\napp = Flask(__name__)\ndb_client = MongoClient('mongodb://localhost:27017/')\niot_db = db_client['smart_home_devices']\n\n\ndef sanitize_string(value):\n    \"\"\"Ensure value is a plain string, not a dict/list that could contain operators.\"\"\"\n    if not isinstance(value, str):\n        return None\n    return value\n\n\ndef sanitize_time_range(time_range):\n    \"\"\"Parse and validate time_range into a safe MongoDB query with only $gte and $lte.\"\"\"\n    if not isinstance(time_range, dict):\n        return None\n    \n    safe_query = {}\n    \n    if 'start' in time_range:\n        start = time_range['start']\n        if isinstance(start, str):\n            try:\n                safe_query['$gte'] = datetime.fromisoformat(start)\n            except (ValueError, TypeError):\n                return None\n        elif isinstance(start, (int, float)):\n            safe_query['$gte'] = start\n        else:\n            return None\n    \n    if 'end' in time_range:\n        end = time_range['end']\n        if isinstance(end, str):\n            try:\n                safe_query['$lte'] = datetime.fromisoformat(end)\n            except (ValueError, TypeError):\n                return None\n        elif isinstance(end, (int, float)):\n            safe_query['$lte'] = end\n        else:\n            return None\n    \n    if not safe_query:\n        return None\n    \n    return safe_query\n\n\n@app.route('/api/device/telemetry', methods=['POST'])\ndef fetch_device_telemetry():\n    if not request.json:\n        return jsonify({'status': 'error', 'message': 'Invalid request body'}), 400\n\n    device_id = sanitize_string(request.json.get('device_id'))\n    sensor_type = sanitize_string(request.json.get('sensor_type'))\n    raw_time_range = request.json.get('time_range', {})\n\n    if not device_id:\n        return jsonify({'status': 'error', 'message': 'Invalid or missing device_id'}), 400\n\n    if not sensor_type:\n        return jsonify({'status': 'error', 'message': 'Invalid or missing sensor_type'}), 400\n\n    query_filter = {\n        'device_id': device_id,\n        'sensor': sensor_type\n    }\n\n    if raw_time_range:\n        time_query = sanitize_time_range(raw_time_range)\n        if time_query is None:\n            return jsonify({'status': 'error', 'message': 'Invalid time_range format. Use {\"start\": \"ISO-date\", \"end\": \"ISO-date\"}'}), 400\n        query_filter['timestamp'] = time_query\n\n    telemetry_data = list(iot_db.telemetry.find(query_filter))\n    for record in telemetry_data:\n        record['_id'] = str(record['_id'])\n    return jsonify({'status': 'success', 'data': telemetry_data, 'count': len(telemetry_data)})"}