# Uncontrolled Resource Consumption via ast.literal_eval Misuse

Language: Python
Severity: Medium
CWE: CWE-400

## Source
9

## Flow
9-11

## Sink
11

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

app = Flask(__name__)

@app.route('/iot/device/configure', methods=['POST'])
def update_device_config():
    device_id = request.json.get('device_id')
    sensor_thresholds = request.json.get('thresholds', '{}')
    try:
        parsed_config = ast.literal_eval(sensor_thresholds)
        device_settings = {'device': device_id, 'config': parsed_config}
        return jsonify({'status': 'configured', 'settings': device_settings})
    except:
        return jsonify({'error': 'Invalid configuration format'}), 400
```

## Explanation

The application uses ast.literal_eval() on user-controlled string input retrieved from request.json.get('thresholds'). While ast.literal_eval() is safer than eval() and cannot execute arbitrary code expressions, it remains vulnerable to Denial of Service attacks via deeply nested or resource-intensive literal structures (e.g., deeply nested lists or dicts). Additionally, using ast.literal_eval() instead of json.loads() for JSON API input is an inappropriate use of the function, introducing unnecessary parsing risk. There is no input size validation, no structural validation, and no type enforcement on the parsed result, leaving the endpoint open to resource exhaustion attacks.

## Remediation

The fix replaces ast.literal_eval() with json.loads() for parsing string input, which is semantically correct for a JSON API and avoids Python-specific literal parsing risks. Additionally, strict input validation is applied including an input size limit (MAX_INPUT_LENGTH) to prevent DoS via deeply nested structures, a whitelist of allowed threshold keys, numeric type and range checking for threshold values, device_id format validation, and a Content-Type enforcement check.

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

app = Flask(__name__)

ALLOWED_THRESHOLD_KEYS = {'temperature', 'humidity', 'pressure', 'temp', 'hum', 'pres'}
MAX_THRESHOLD_VALUE = 10000
MIN_THRESHOLD_VALUE = -10000
MAX_INPUT_LENGTH = 1024

def validate_thresholds(thresholds):
    """Validate that thresholds is a flat dictionary with allowed keys and numeric values."""
    if not isinstance(thresholds, dict):
        return False, "Thresholds must be a JSON object"
    if len(thresholds) > 20:
        return False, "Too many threshold entries"
    for key, value in thresholds.items():
        if key not in ALLOWED_THRESHOLD_KEYS:
            return False, f"Unknown threshold key: {key}"
        if not isinstance(value, (int, float)):
            return False, f"Threshold value for '{key}' must be a number"
        if value < MIN_THRESHOLD_VALUE or value > MAX_THRESHOLD_VALUE:
            return False, f"Threshold value for '{key}' is out of allowed range"
    return True, None

@app.route('/iot/device/configure', methods=['POST'])
def update_device_config():
    if not request.is_json:
        return jsonify({'error': 'Content-Type must be application/json'}), 400

    device_id = request.json.get('device_id')
    if not isinstance(device_id, str) or len(device_id) > 128:
        return jsonify({'error': 'Invalid device_id'}), 400

    sensor_thresholds = request.json.get('thresholds', {})

    if isinstance(sensor_thresholds, str):
        if len(sensor_thresholds) > MAX_INPUT_LENGTH:
            return jsonify({'error': 'Configuration input too large'}), 400
        try:
            sensor_thresholds = json.loads(sensor_thresholds)
        except json.JSONDecodeError:
            return jsonify({'error': 'Invalid JSON in thresholds'}), 400

    is_valid, error_msg = validate_thresholds(sensor_thresholds)
    if not is_valid:
        return jsonify({'error': error_msg}), 400

    device_settings = {'device': device_id, 'config': sensor_thresholds}
    return jsonify({'status': 'configured', 'settings': device_settings})
```
