{"title":"Code Injection via eval on Untrusted Input","language":"Python","severity":"Critical","cwe":"CWE-94","source_lines":[1],"flow_lines":[1,2,3,4,5,6],"sink_lines":[6],"vulnerable_code":"def apply_iot_device_threshold(device_id, threshold_expr):\n    sensor_data = fetch_sensor_readings(device_id)\n    max_temp = sensor_data.get('temperature', 0)\n    max_humidity = sensor_data.get('humidity', 0)\n    max_pressure = sensor_data.get('pressure', 0)\n    alert_condition = eval(threshold_expr)\n    if alert_condition:\n        trigger_device_alert(device_id, \"Threshold exceeded\")\n        return {\"status\": \"alert_triggered\", \"condition\": threshold_expr}\n    return {\"status\": \"normal\", \"condition\": threshold_expr}","explanation":"The function accepts untrusted user input via the 'threshold_expr' parameter and directly passes it to the eval() function without any validation or sanitization. This allows an attacker to execute arbitrary Python code by injecting malicious expressions through the threshold configuration interface.","remediation":"The fix replaces the dangerous eval() call with a custom safe expression evaluator built on Python's ast module. The safe_eval_threshold function parses the expression into an AST and only evaluates nodes that are explicitly whitelisted: numeric constants, approved variable names (max_temp, max_humidity, max_pressure), basic arithmetic operators, comparison operators, and boolean logic (and/or). Any attempt to use function calls, attribute access, imports, or other dangerous constructs will raise a ValueError.","secure_code":"import ast\nimport operator\n\nALLOWED_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\nALLOWED_COMPARISONS = {\n    ast.Gt: operator.gt,\n    ast.Lt: operator.lt,\n    ast.GtE: operator.ge,\n    ast.LtE: operator.le,\n    ast.Eq: operator.eq,\n    ast.NotEq: operator.ne,\n}\n\nALLOWED_BOOL_OPS = {\n    ast.And: lambda values: all(values),\n    ast.Or: lambda values: any(values),\n}\n\n\ndef safe_eval_threshold(expr, variables):\n    \"\"\"Safely evaluate a threshold expression using AST parsing.\n    Only allows arithmetic operations, comparisons, boolean logic, and whitelisted variable names.\"\"\"\n    try:\n        tree = ast.parse(expr, mode='eval')\n    except SyntaxError:\n        raise ValueError(f\"Invalid threshold expression syntax: {expr}\")\n\n    def _eval_node(node):\n        if isinstance(node, ast.Expression):\n            return _eval_node(node.body)\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\"Unsupported variable name: {node.id}\")\n        elif isinstance(node, ast.BinOp):\n            op_type = type(node.op)\n            if op_type not in ALLOWED_OPERATORS:\n                raise ValueError(f\"Unsupported binary operator: {op_type.__name__}\")\n            left = _eval_node(node.left)\n            right = _eval_node(node.right)\n            return ALLOWED_OPERATORS[op_type](left, right)\n        elif isinstance(node, ast.UnaryOp):\n            op_type = type(node.op)\n            if op_type not in ALLOWED_OPERATORS:\n                raise ValueError(f\"Unsupported unary operator: {op_type.__name__}\")\n            operand = _eval_node(node.operand)\n            return ALLOWED_OPERATORS[op_type](operand)\n        elif isinstance(node, ast.Compare):\n            left = _eval_node(node.left)\n            result = True\n            for op, comparator in zip(node.ops, node.comparators):\n                op_type = type(op)\n                if op_type not in ALLOWED_COMPARISONS:\n                    raise ValueError(f\"Unsupported comparison operator: {op_type.__name__}\")\n                right = _eval_node(comparator)\n                if not ALLOWED_COMPARISONS[op_type](left, right):\n                    result = False\n                    break\n                left = right\n            return result\n        elif isinstance(node, ast.BoolOp):\n            op_type = type(node.op)\n            if op_type not in ALLOWED_BOOL_OPS:\n                raise ValueError(f\"Unsupported boolean operator: {op_type.__name__}\")\n            values = [_eval_node(v) for v in node.values]\n            return ALLOWED_BOOL_OPS[op_type](values)\n        elif isinstance(node, ast.IfExp):\n            raise ValueError(\"Conditional expressions are not allowed\")\n        else:\n            raise ValueError(f\"Unsupported expression node: {type(node).__name__}\")\n\n    return _eval_node(tree)\n\n\ndef apply_iot_device_threshold(device_id, threshold_expr):\n    sensor_data = fetch_sensor_readings(device_id)\n    max_temp = sensor_data.get('temperature', 0)\n    max_humidity = sensor_data.get('humidity', 0)\n    max_pressure = sensor_data.get('pressure', 0)\n\n    allowed_variables = {\n        'max_temp': max_temp,\n        'max_humidity': max_humidity,\n        'max_pressure': max_pressure,\n    }\n\n    try:\n        alert_condition = safe_eval_threshold(threshold_expr, allowed_variables)\n    except ValueError as e:\n        return {\"status\": \"error\", \"message\": str(e)}\n\n    if alert_condition:\n        trigger_device_alert(device_id, \"Threshold exceeded\")\n        return {\"status\": \"alert_triggered\", \"condition\": threshold_expr}\n    return {\"status\": \"normal\", \"condition\": threshold_expr}"}