# MongoDB NoSQL Injection via Unvalidated Query Operators

Language: Python
Severity: High
CWE: CWE-943

## Source
10-11

## Flow
10-11-12

## Sink
12

## Vulnerable Code
```python
from flask import Flask, request, jsonify
from pymongo import MongoClient

app = Flask(__name__)
db = MongoClient('mongodb://localhost:27017/').iot_devices

@app.route('/api/device/telemetry', methods=['POST'])
def fetch_device_telemetry():
    device_id = request.json.get('device_id')
    time_range = request.json.get('time_range', {})
    telemetry_data = db.telemetry.find({'device_id': device_id, 'timestamp': time_range})
    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
```python
from flask import Flask, request, jsonify
from pymongo import MongoClient
from datetime import datetime

app = Flask(__name__)
db = MongoClient('mongodb://localhost:27017/').iot_devices

def validate_device_id(device_id):
    """Ensure device_id is a plain string, not a query operator."""
    if not isinstance(device_id, str):
        return None
    if device_id.startswith('$'):
        return None
    return device_id

def validate_time_range(time_range):
    """Parse and validate time_range into a safe MongoDB query using only whitelisted operators."""
    if time_range is None:
        return {}
    
    if not isinstance(time_range, dict):
        return None
    
    safe_query = {}
    allowed_operators = {'$gt', '$gte', '$lt', '$lte'}
    
    for key, value in time_range.items():
        if key not in allowed_operators:
            return None
        if isinstance(value, (int, float)):
            safe_query[key] = value
        elif isinstance(value, str):
            try:
                safe_query[key] = datetime.fromisoformat(value)
            except (ValueError, TypeError):
                return None
        else:
            return None
    
    return safe_query

@app.route('/api/device/telemetry', methods=['POST'])
def fetch_device_telemetry():
    if not request.json:
        return jsonify({'error': 'Invalid request body'}), 400
    
    device_id = validate_device_id(request.json.get('device_id'))
    if device_id is None:
        return jsonify({'error': 'Invalid device_id parameter'}), 400
    
    time_range_input = request.json.get('time_range', {})
    time_range = validate_time_range(time_range_input)
    if time_range is None:
        return jsonify({'error': 'Invalid time_range parameter. Use {"$gte": "ISO_DATE", "$lte": "ISO_DATE"} format.'}), 400
    
    query = {'device_id': device_id}
    if time_range:
        query['timestamp'] = time_range
    
    telemetry_data = db.telemetry.find(query, {'_id': 0})
    return jsonify([doc for doc in telemetry_data])
```
