# Insecure Deserialization via `ast.literal_eval()` on Untrusted Input

Language: Python
Severity: Medium
CWE: CWE-502

## Source
8

## Flow
8-9

## Sink
9

## Vulnerable Code
```python
from flask import Flask, request, jsonify
import ast
app = Flask(__name__)
@app.route('/iot/device/config', methods=['POST'])
def update_device_config():
    device_id = request.form.get('device_id')
    config_payload = request.form.get('config_data')
    try:
        parsed_config = ast.literal_eval(config_payload)
        device_settings = {'device': device_id, 'config': parsed_config}
        return jsonify({'status': 'success', 'updated': device_settings}), 200
    except Exception as e:
        return jsonify({'status': 'error', 'message': str(e)}), 400
```

## Explanation

The application accepts untrusted user input from request.form.get('config_data') at line 8 and directly passes it to ast.literal_eval() at line 9 without validation. While ast.literal_eval() is safer than eval(), it can still cause Denial of Service through deeply nested structures or consume excessive memory with large literals, enabling resource exhaustion attacks.

## Remediation

The fix replaces ast.literal_eval() with json.loads() which is the standard and safe way to parse structured data from untrusted input. Additionally, it adds input size validation to prevent memory exhaustion, nesting depth checks to prevent stack overflow from deeply nested structures, and type validation to ensure the parsed config is a valid object or array.

## Secure Code
```python
from flask import Flask, request, jsonify
import json

app = Flask(__name__)

MAX_CONFIG_SIZE = 10000  # Maximum allowed config payload size in characters
MAX_NESTING_DEPTH = 10  # Maximum allowed nesting depth


def check_nesting_depth(obj, max_depth, current_depth=0):
    if current_depth > max_depth:
        return False
    if isinstance(obj, dict):
        for key, value in obj.items():
            if not check_nesting_depth(value, max_depth, current_depth + 1):
                return False
    elif isinstance(obj, (list, tuple)):
        for item in obj:
            if not check_nesting_depth(item, max_depth, current_depth + 1):
                return False
    return True


@app.route('/iot/device/config', methods=['POST'])
def update_device_config():
    device_id = request.form.get('device_id')
    config_payload = request.form.get('config_data')

    if not config_payload:
        return jsonify({'status': 'error', 'message': 'Missing config_data'}), 400

    if len(config_payload) > MAX_CONFIG_SIZE:
        return jsonify({'status': 'error', 'message': 'Config payload exceeds maximum allowed size'}), 400

    try:
        parsed_config = json.loads(config_payload)
    except (json.JSONDecodeError, ValueError) as e:
        return jsonify({'status': 'error', 'message': 'Invalid JSON configuration data'}), 400

    if not isinstance(parsed_config, (dict, list)):
        return jsonify({'status': 'error', 'message': 'Config must be a JSON object or array'}), 400

    if not check_nesting_depth(parsed_config, MAX_NESTING_DEPTH):
        return jsonify({'status': 'error', 'message': 'Config nesting depth exceeds maximum allowed'}), 400

    device_settings = {'device': device_id, 'config': parsed_config}
    return jsonify({'status': 'success', 'updated': device_settings}), 200
```
