{"title":"Code Injection via eval() on Untrusted Expression","language":"Python","severity":"Critical","cwe":"CWE-95","source_lines":[4],"flow_lines":[4,10],"sink_lines":[10],"vulnerable_code":"@app.route('/iot/device/metrics', methods=['POST'])\ndef calculate_device_metrics():\n    device_id = request.json.get('device_id')\n    metric_formula = request.json.get('formula')\n    sensor_temp = float(request.json.get('temperature', 0))\n    sensor_humid = float(request.json.get('humidity', 0))\n    sensor_pressure = float(request.json.get('pressure', 0))\n    context = {'temp': sensor_temp, 'humid': sensor_humid, 'press': sensor_pressure}\n    try:\n        computed_value = eval(metric_formula, {'__builtins__': {}}, context)\n        db.store_metric(device_id, computed_value)\n        return jsonify({'device': device_id, 'result': computed_value})\n    except Exception as e:\n        return jsonify({'error': 'Calculation failed'}), 400","explanation":"The user-supplied 'formula' parameter from the JSON request is passed directly to eval() without proper sanitization. Although __builtins__ is restricted, attackers can still exploit Python's object introspection capabilities to access dangerous functions through the allowed context variables and break out of the sandbox.","remediation":"The fix replaces the dangerous eval() call with a custom AST-based expression evaluator that parses the formula into an abstract syntax tree and only allows safe mathematical operations (arithmetic operators, numeric constants, whitelisted variable names, and a few safe built-in functions). This completely eliminates the possibility of code injection since no arbitrary Python code can be executed—only explicitly permitted mathematical constructs are evaluated.","secure_code":"import ast\nimport operator\n\n# Safe mathematical operations whitelist\nSAFE_OPERATORS = {\n    ast.Add: operator.add,\n    ast.Sub: operator.sub,\n    ast.Mult: operator.mul,\n    ast.Div: operator.truediv,\n    ast.FloorDiv: operator.floordiv,\n    ast.Mod: operator.mod,\n    ast.Pow: operator.pow,\n    ast.USub: operator.neg,\n    ast.UAdd: operator.pos,\n}\n\nSAFE_FUNCTIONS = {\n    'abs': abs,\n    'round': round,\n    'min': min,\n    'max': max,\n}\n\n\ndef safe_eval_formula(formula, variables):\n    \"\"\"Safely evaluate a mathematical formula using AST parsing.\"\"\"\n    try:\n        tree = ast.parse(formula, mode='eval')\n    except SyntaxError:\n        raise ValueError(\"Invalid formula syntax\")\n\n    return _eval_node(tree.body, variables)\n\n\ndef _eval_node(node, variables):\n    \"\"\"Recursively evaluate AST nodes, only allowing safe operations.\"\"\"\n    if isinstance(node, ast.Expression):\n        return _eval_node(node.body, variables)\n    elif isinstance(node, ast.Constant):\n        if isinstance(node.value, (int, float)):\n            return node.value\n        raise ValueError(f\"Unsupported constant type: {type(node.value)}\")\n    elif isinstance(node, ast.Name):\n        if node.id in variables:\n            return variables[node.id]\n        raise ValueError(f\"Unknown variable: {node.id}\")\n    elif isinstance(node, ast.BinOp):\n        op_type = type(node.op)\n        if op_type not in SAFE_OPERATORS:\n            raise ValueError(f\"Unsupported operator: {op_type.__name__}\")\n        left = _eval_node(node.left, variables)\n        right = _eval_node(node.right, variables)\n        if op_type == ast.Pow and right > 100:\n            raise ValueError(\"Exponent too large\")\n        return SAFE_OPERATORS[op_type](left, right)\n    elif isinstance(node, ast.UnaryOp):\n        op_type = type(node.op)\n        if op_type not in SAFE_OPERATORS:\n            raise ValueError(f\"Unsupported unary operator: {op_type.__name__}\")\n        operand = _eval_node(node.operand, variables)\n        return SAFE_OPERATORS[op_type](operand)\n    elif isinstance(node, ast.Call):\n        if isinstance(node.func, ast.Name) and node.func.id in SAFE_FUNCTIONS:\n            args = [_eval_node(arg, variables) for arg in node.args]\n            if node.keywords:\n                raise ValueError(\"Keyword arguments not supported\")\n            return SAFE_FUNCTIONS[node.func.id](*args)\n        raise ValueError(f\"Unsupported function call\")\n    elif isinstance(node, ast.IfExp):\n        test = _eval_node(node.test, variables)\n        if test:\n            return _eval_node(node.body, variables)\n        return _eval_node(node.orelse, variables)\n    elif isinstance(node, ast.Compare):\n        left = _eval_node(node.left, variables)\n        for op, comparator in zip(node.ops, node.comparators):\n            right = _eval_node(comparator, variables)\n            if isinstance(op, ast.Gt):\n                result = left > right\n            elif isinstance(op, ast.Lt):\n                result = left < right\n            elif isinstance(op, ast.GtE):\n                result = left >= right\n            elif isinstance(op, ast.LtE):\n                result = left <= right\n            elif isinstance(op, ast.Eq):\n                result = left == right\n            elif isinstance(op, ast.NotEq):\n                result = left != right\n            else:\n                raise ValueError(f\"Unsupported comparison operator\")\n            if not result:\n                return False\n            left = right\n        return True\n    else:\n        raise ValueError(f\"Unsupported expression type: {type(node).__name__}\")\n\n\n@app.route('/iot/device/metrics', methods=['POST'])\ndef calculate_device_metrics():\n    device_id = request.json.get('device_id')\n    metric_formula = request.json.get('formula')\n    if not metric_formula or not isinstance(metric_formula, str):\n        return jsonify({'error': 'Invalid formula'}), 400\n    if len(metric_formula) > 500:\n        return jsonify({'error': 'Formula too long'}), 400\n    sensor_temp = float(request.json.get('temperature', 0))\n    sensor_humid = float(request.json.get('humidity', 0))\n    sensor_pressure = float(request.json.get('pressure', 0))\n    context = {'temp': sensor_temp, 'humid': sensor_humid, 'press': sensor_pressure}\n    try:\n        computed_value = safe_eval_formula(metric_formula, context)\n        db.store_metric(device_id, computed_value)\n        return jsonify({'device': device_id, 'result': computed_value})\n    except (ValueError, TypeError, ZeroDivisionError) as e:\n        return jsonify({'error': 'Calculation failed'}), 400"}