# Code Injection via eval() on User-Supplied Expression

Language: Python
Severity: Critical
CWE: CWE-94

## Source
4

## Flow
4-6, 4-7

## Sink
6, 7

## Vulnerable Code
```python
@app.route('/iot/device/threshold', methods=['POST'])
def update_sensor_threshold():
    device_id = request.json.get('device_id')
    threshold_expr = request.json.get('threshold_formula')
    sensor_data = {'temp': 25.3, 'humidity': 60, 'pressure': 1013}
    try:
        computed_threshold = eval(threshold_expr, {'__builtins__': {}}, sensor_data)
        db.execute(f"UPDATE iot_devices SET alert_threshold={computed_threshold} WHERE id={device_id}")
        return jsonify({'status': 'success', 'new_threshold': computed_threshold})
    except Exception as e:
        return jsonify({'error': str(e)}), 400
```

## Explanation

Line 4 accepts unsanitized user input 'threshold_formula' which flows to line 6 where eval() executes arbitrary Python code despite namespace restrictions. Additionally, line 7 uses string formatting to insert user-controlled 'device_id' and computed values directly into SQL, enabling SQL injection attacks.

## Remediation

The fix replaces eval() with a safe AST-based expression parser that only allows arithmetic operations (+, -, *, /, **) on numeric constants and whitelisted sensor variable names, preventing arbitrary code execution. The SQL injection vulnerability is fixed by using parameterized queries with placeholders instead of string formatting, and device_id is validated to be an integer.

## Secure Code
```python
import ast
import operator

ALLOWED_OPERATORS = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
    ast.Pow: operator.pow,
    ast.USub: operator.neg,
    ast.UAdd: operator.pos,
}

ALLOWED_VARIABLES = {'temp', 'humidity', 'pressure'}

def safe_eval_expr(expr, variables):
    """Safely evaluate a mathematical expression with allowed variables."""
    tree = ast.parse(expr, mode='eval')
    return _eval_node(tree.body, variables)

def _eval_node(node, variables):
    if isinstance(node, ast.Expression):
        return _eval_node(node.body, variables)
    elif isinstance(node, ast.Constant):
        if isinstance(node.value, (int, float)):
            return node.value
        raise ValueError(f"Unsupported constant type: {type(node.value)}")
    elif isinstance(node, ast.Name):
        if node.id in ALLOWED_VARIABLES and node.id in variables:
            return variables[node.id]
        raise ValueError(f"Unsupported variable: {node.id}")
    elif isinstance(node, ast.BinOp):
        op_type = type(node.op)
        if op_type not in ALLOWED_OPERATORS:
            raise ValueError(f"Unsupported operator: {op_type.__name__}")
        left = _eval_node(node.left, variables)
        right = _eval_node(node.right, variables)
        return ALLOWED_OPERATORS[op_type](left, right)
    elif isinstance(node, ast.UnaryOp):
        op_type = type(node.op)
        if op_type not in ALLOWED_OPERATORS:
            raise ValueError(f"Unsupported unary operator: {op_type.__name__}")
        operand = _eval_node(node.operand, variables)
        return ALLOWED_OPERATORS[op_type](operand)
    else:
        raise ValueError(f"Unsupported expression type: {type(node).__name__}")

@app.route('/iot/device/threshold', methods=['POST'])
def update_sensor_threshold():
    device_id = request.json.get('device_id')
    threshold_expr = request.json.get('threshold_formula')
    sensor_data = {'temp': 25.3, 'humidity': 60, 'pressure': 1013}
    try:
        if not isinstance(threshold_expr, str) or len(threshold_expr) > 200:
            return jsonify({'error': 'Invalid threshold formula'}), 400
        if not isinstance(device_id, int):
            return jsonify({'error': 'Invalid device_id, must be an integer'}), 400
        computed_threshold = safe_eval_expr(threshold_expr, sensor_data)
        if not isinstance(computed_threshold, (int, float)):
            return jsonify({'error': 'Formula must evaluate to a number'}), 400
        db.execute(
            "UPDATE iot_devices SET alert_threshold = ? WHERE id = ?",
            (float(computed_threshold), int(device_id))
        )
        return jsonify({'status': 'success', 'new_threshold': computed_threshold})
    except Exception as e:
        return jsonify({'error': str(e)}), 400
```
