# NoSQL Injection via MongoDB Query Operator Injection

Language: Python
Severity: High
CWE: CWE-943

## Source
9-11

## Flow
9-11-12-15-17

## Sink
17

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

app = Flask(__name__)
db_client = MongoClient('mongodb://localhost:27017/')
iot_db = db_client['smart_home_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', {})
    sensor_type = request.json.get('sensor_type')
    query_filter = {
        'device_id': device_id,
        'timestamp': time_range,
        'sensor': sensor_type
    }
    telemetry_data = list(iot_db.telemetry.find(query_filter))
    for record in telemetry_data:
        record['_id'] = str(record['_id'])
    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
```python
from flask import Flask, request, jsonify
from pymongo import MongoClient
from datetime import datetime

app = Flask(__name__)
db_client = MongoClient('mongodb://localhost:27017/')
iot_db = db_client['smart_home_devices']


def sanitize_string(value):
    """Ensure value is a plain string, not a dict/list that could contain operators."""
    if not isinstance(value, str):
        return None
    return value


def sanitize_time_range(time_range):
    """Parse and validate time_range into a safe MongoDB query with only $gte and $lte."""
    if not isinstance(time_range, dict):
        return None
    
    safe_query = {}
    
    if 'start' in time_range:
        start = time_range['start']
        if isinstance(start, str):
            try:
                safe_query['$gte'] = datetime.fromisoformat(start)
            except (ValueError, TypeError):
                return None
        elif isinstance(start, (int, float)):
            safe_query['$gte'] = start
        else:
            return None
    
    if 'end' in time_range:
        end = time_range['end']
        if isinstance(end, str):
            try:
                safe_query['$lte'] = datetime.fromisoformat(end)
            except (ValueError, TypeError):
                return None
        elif isinstance(end, (int, float)):
            safe_query['$lte'] = end
        else:
            return None
    
    if not safe_query:
        return None
    
    return safe_query


@app.route('/api/device/telemetry', methods=['POST'])
def fetch_device_telemetry():
    if not request.json:
        return jsonify({'status': 'error', 'message': 'Invalid request body'}), 400

    device_id = sanitize_string(request.json.get('device_id'))
    sensor_type = sanitize_string(request.json.get('sensor_type'))
    raw_time_range = request.json.get('time_range', {})

    if not device_id:
        return jsonify({'status': 'error', 'message': 'Invalid or missing device_id'}), 400

    if not sensor_type:
        return jsonify({'status': 'error', 'message': 'Invalid or missing sensor_type'}), 400

    query_filter = {
        'device_id': device_id,
        'sensor': sensor_type
    }

    if raw_time_range:
        time_query = sanitize_time_range(raw_time_range)
        if time_query is None:
            return jsonify({'status': 'error', 'message': 'Invalid time_range format. Use {"start": "ISO-date", "end": "ISO-date"}'}), 400
        query_filter['timestamp'] = time_query

    telemetry_data = list(iot_db.telemetry.find(query_filter))
    for record in telemetry_data:
        record['_id'] = str(record['_id'])
    return jsonify({'status': 'success', 'data': telemetry_data, 'count': len(telemetry_data)})
```
