{"title":"Arbitrary Code Execution via unsafe eval() on Untrusted Expressions","language":"Python","severity":"Critical","cwe":"CWE-95","source_lines":[4],"flow_lines":[4,8],"sink_lines":[8],"vulnerable_code":"@app.route('/iot/device/threshold', methods=['POST'])\ndef configure_sensor_threshold():\n    device_id = request.json.get('device_id')\n    threshold_expr = request.json.get('threshold_expression')\n    sensor_data = iot_db.get_latest_reading(device_id)\n    context = {'temp': sensor_data['temperature'], 'humidity': sensor_data['humidity'], 'pressure': sensor_data['pressure']}\n    try:\n        alert_triggered = eval(threshold_expr, {'__builtins__': {}}, context)\n        if alert_triggered:\n            trigger_iot_alert(device_id, sensor_data)\n        return jsonify({'status': 'threshold_evaluated', 'alert': bool(alert_triggered)})\n    except Exception as e:\n        return jsonify({'error': 'evaluation_failed'}), 400","explanation":"The application accepts untrusted user input via 'threshold_expression' and directly passes it to eval() on line 8. Although __builtins__ is restricted, this protection is bypassable using techniques like accessing object.__subclasses__() through the context variables, allowing arbitrary code execution.","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 recursively evaluates only explicitly allowed node types: comparisons, basic arithmetic, boolean logic, numeric constants, and a whitelist of variable names (temp, humidity, pressure). Any attempt to use function calls, attribute access, imports, or other dangerous constructs is rejected.","secure_code":"import ast\nimport operator\n\nALLOWED_OPERATORS = {\n    ast.Lt: operator.lt,\n    ast.LtE: operator.le,\n    ast.Gt: operator.gt,\n    ast.GtE: operator.ge,\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\nALLOWED_UNARY_OPS = {\n    ast.Not: operator.not_,\n    ast.USub: operator.neg,\n}\n\nALLOWED_BIN_OPS = {\n    ast.Add: operator.add,\n    ast.Sub: operator.sub,\n    ast.Mult: operator.mul,\n    ast.Div: operator.truediv,\n}\n\nALLOWED_VARIABLES = {'temp', 'humidity', 'pressure'}\n\n\ndef safe_eval_threshold(expr_str, context):\n    \"\"\"Safely evaluate a threshold expression using AST parsing.\n    Only allows comparisons, basic arithmetic, boolean logic, numbers, and allowed variable names.\"\"\"\n    try:\n        tree = ast.parse(expr_str, mode='eval')\n    except SyntaxError:\n        raise ValueError(\"Invalid expression syntax\")\n\n    def _eval_node(node):\n        if isinstance(node, ast.Expression):\n            return _eval_node(node.body)\n        elif isinstance(node, ast.Compare):\n            left = _eval_node(node.left)\n            for op, comparator in zip(node.ops, node.comparators):\n                op_type = type(op)\n                if op_type not in ALLOWED_OPERATORS:\n                    raise ValueError(f\"Disallowed comparison operator: {op_type.__name__}\")\n                right = _eval_node(comparator)\n                if not ALLOWED_OPERATORS[op_type](left, right):\n                    return False\n                left = right\n            return True\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\"Disallowed 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.UnaryOp):\n            op_type = type(node.op)\n            if op_type not in ALLOWED_UNARY_OPS:\n                raise ValueError(f\"Disallowed unary operator: {op_type.__name__}\")\n            operand = _eval_node(node.operand)\n            return ALLOWED_UNARY_OPS[op_type](operand)\n        elif isinstance(node, ast.BinOp):\n            op_type = type(node.op)\n            if op_type not in ALLOWED_BIN_OPS:\n                raise ValueError(f\"Disallowed binary operator: {op_type.__name__}\")\n            left = _eval_node(node.left)\n            right = _eval_node(node.right)\n            return ALLOWED_BIN_OPS[op_type](left, right)\n        elif isinstance(node, ast.Num):\n            return node.n\n        elif isinstance(node, ast.Constant):\n            if isinstance(node.value, (int, float)):\n                return node.value\n            raise ValueError(f\"Disallowed constant type: {type(node.value).__name__}\")\n        elif isinstance(node, ast.Name):\n            if node.id not in ALLOWED_VARIABLES:\n                raise ValueError(f\"Disallowed variable: {node.id}\")\n            if node.id not in context:\n                raise ValueError(f\"Unknown variable: {node.id}\")\n            return context[node.id]\n        else:\n            raise ValueError(f\"Disallowed expression type: {type(node).__name__}\")\n\n    return _eval_node(tree)\n\n\n@app.route('/iot/device/threshold', methods=['POST'])\ndef configure_sensor_threshold():\n    device_id = request.json.get('device_id')\n    threshold_expr = request.json.get('threshold_expression')\n    if not threshold_expr or not isinstance(threshold_expr, str):\n        return jsonify({'error': 'invalid_expression'}), 400\n    if len(threshold_expr) > 200:\n        return jsonify({'error': 'expression_too_long'}), 400\n    sensor_data = iot_db.get_latest_reading(device_id)\n    context = {'temp': sensor_data['temperature'], 'humidity': sensor_data['humidity'], 'pressure': sensor_data['pressure']}\n    try:\n        alert_triggered = safe_eval_threshold(threshold_expr, context)\n        if alert_triggered:\n            trigger_iot_alert(device_id, sensor_data)\n        return jsonify({'status': 'threshold_evaluated', 'alert': bool(alert_triggered)})\n    except ValueError as e:\n        return jsonify({'error': 'evaluation_failed', 'detail': str(e)}), 400\n    except Exception as e:\n        return jsonify({'error': 'evaluation_failed'}), 400"}