# NoSQL Injection via Unvalidated MongoDB Query Operators

Language: Python
Severity: High
CWE: CWE-943

## Source
10-11

## Flow
10-11-12-13

## Sink
13

## 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_query = {'device_id': device_id, 'timestamp': time_range}
    results = db.telemetry.find(telemetry_query)
    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
```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 sanitize_time_range(time_range):
    """Validate and sanitize time_range to only allow safe timestamp values."""
    if not isinstance(time_range, dict):
        return None
    
    sanitized = {}
    allowed_operators = {'$gt', '$gte', '$lt', '$lte'}
    
    for key, value in time_range.items():
        if key not in allowed_operators:
            continue
        if isinstance(value, (int, float)):
            sanitized[key] = value
        elif isinstance(value, str):
            try:
                parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))
                sanitized[key] = value
            except (ValueError, AttributeError):
                continue
    
    return sanitized if sanitized else None

def sanitize_device_id(device_id):
    """Ensure device_id is a plain string, not an operator object."""
    if not isinstance(device_id, str):
        return None
    return device_id

@app.route('/api/device/telemetry', methods=['POST'])
def fetch_device_telemetry():
    if not request.json:
        return jsonify({'error': 'Invalid request body'}), 400
    
    device_id = sanitize_device_id(request.json.get('device_id'))
    if not device_id:
        return jsonify({'error': 'Invalid device_id: must be a non-empty string'}), 400
    
    time_range_input = request.json.get('time_range', {})
    
    telemetry_query = {'device_id': device_id}
    
    if time_range_input:
        sanitized_time_range = sanitize_time_range(time_range_input)
        if sanitized_time_range is None:
            return jsonify({'error': 'Invalid time_range: must contain valid timestamp operators ($gt, $gte, $lt, $lte) with numeric or ISO datetime string values'}), 400
        telemetry_query['timestamp'] = sanitized_time_range
    
    results = db.telemetry.find(telemetry_query, {'_id': 0})
    return jsonify([doc for doc in results])
```
