{"title":"Code Injection via eval() on Untrusted Input","language":"Python","severity":"Critical","cwe":"CWE-95","source_lines":[4],"flow_lines":[4,6],"sink_lines":[6],"vulnerable_code":"@app.route('/iot/device/configure', methods=['POST'])\ndef apply_device_config():\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    try:\n        computed_threshold = eval(threshold_expr, {'sensor': sensor_data, 'math': __import__('math')})\n        if sensor_data['value'] > computed_threshold:\n            trigger_alert(device_id)\n            return jsonify({'status': 'alert_triggered', 'threshold': computed_threshold})\n        return jsonify({'status': 'normal', 'threshold': computed_threshold})\n    except Exception as e:\n        return jsonify({'error': 'Invalid threshold expression'}), 400","explanation":"The application accepts user-controlled input 'threshold_expression' and directly passes it to eval() function. Although the eval() uses a restricted namespace with only 'sensor' and 'math', attackers can escape this restriction using Python introspection techniques like __import__, __builtins__, or accessing object.__subclasses__() to execute arbitrary code.","remediation":"The fix replaces the dangerous eval() call with a custom AST-based safe expression evaluator that parses the expression into an abstract syntax tree and only allows specific, whitelisted operations: basic arithmetic operators, a curated set of math functions, math constants, and access to sensor data fields. This approach makes it impossible for attackers to execute arbitrary code, import modules, or access Python internals since only explicitly permitted node types are evaluated.","secure_code":"import ast\nimport operator\nimport math\n\n# Safe mathematical expression evaluator\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_MATH_FUNCTIONS = {\n    'sqrt': math.sqrt,\n    'log': math.log,\n    'log10': math.log10,\n    'exp': math.exp,\n    'ceil': math.ceil,\n    'floor': math.floor,\n    'abs': abs,\n    'min': min,\n    'max': max,\n    'pow': pow,\n    'round': round,\n}\n\nSAFE_MATH_CONSTANTS = {\n    'pi': math.pi,\n    'e': math.e,\n}\n\n\ndef safe_eval_expression(expr, sensor_data):\n    \"\"\"Safely evaluate a mathematical expression with sensor data context.\"\"\"\n    try:\n        tree = ast.parse(expr, mode='eval')\n    except SyntaxError:\n        raise ValueError(\"Invalid expression syntax\")\n\n    return _eval_node(tree.body, sensor_data)\n\n\ndef _eval_node(node, sensor_data):\n    if 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\n    elif isinstance(node, ast.BinOp):\n        left = _eval_node(node.left, sensor_data)\n        right = _eval_node(node.right, sensor_data)\n        op_type = type(node.op)\n        if op_type not in SAFE_OPERATORS:\n            raise ValueError(f\"Unsupported operator: {op_type.__name__}\")\n        if op_type == ast.Pow and right > 100:\n            raise ValueError(\"Exponent too large\")\n        return SAFE_OPERATORS[op_type](left, right)\n\n    elif isinstance(node, ast.UnaryOp):\n        operand = _eval_node(node.operand, sensor_data)\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        return SAFE_OPERATORS[op_type](operand)\n\n    elif isinstance(node, ast.Call):\n        if isinstance(node.func, ast.Name) and node.func.id in SAFE_MATH_FUNCTIONS:\n            args = [_eval_node(arg, sensor_data) for arg in node.args]\n            return SAFE_MATH_FUNCTIONS[node.func.id](*args)\n        raise ValueError(f\"Unsupported function call\")\n\n    elif isinstance(node, ast.Name):\n        name = node.id\n        if name in SAFE_MATH_CONSTANTS:\n            return SAFE_MATH_CONSTANTS[name]\n        raise ValueError(f\"Unsupported variable: {name}\")\n\n    elif isinstance(node, ast.Attribute):\n        if isinstance(node.value, ast.Name) and node.value.id == 'sensor':\n            attr = node.attr\n            if attr in sensor_data:\n                value = sensor_data[attr]\n                if isinstance(value, (int, float)):\n                    return value\n                raise ValueError(f\"Sensor attribute '{attr}' is not numeric\")\n            raise ValueError(f\"Unknown sensor attribute: {attr}\")\n        raise ValueError(\"Unsupported attribute access\")\n\n    elif isinstance(node, ast.Subscript):\n        if isinstance(node.value, ast.Name) and node.value.id == 'sensor':\n            if isinstance(node.slice, ast.Constant) and isinstance(node.slice.value, str):\n                key = node.slice.value\n                if key in sensor_data:\n                    value = sensor_data[key]\n                    if isinstance(value, (int, float)):\n                        return value\n                    raise ValueError(f\"Sensor key '{key}' is not numeric\")\n                raise ValueError(f\"Unknown sensor key: {key}\")\n        raise ValueError(\"Unsupported subscript access\")\n\n    else:\n        raise ValueError(f\"Unsupported expression element: {type(node).__name__}\")\n\n\n@app.route('/iot/device/configure', methods=['POST'])\ndef apply_device_config():\n    device_id = request.json.get('device_id')\n    threshold_expr = request.json.get('threshold_expression')\n\n    if not device_id or not threshold_expr:\n        return jsonify({'error': 'Missing required fields'}), 400\n\n    if len(threshold_expr) > 200:\n        return jsonify({'error': 'Expression too long'}), 400\n\n    sensor_data = iot_db.get_latest_reading(device_id)\n    try:\n        computed_threshold = safe_eval_expression(threshold_expr, sensor_data)\n        if not isinstance(computed_threshold, (int, float)):\n            return jsonify({'error': 'Expression must evaluate to a number'}), 400\n        if sensor_data['value'] > computed_threshold:\n            trigger_alert(device_id)\n            return jsonify({'status': 'alert_triggered', 'threshold': computed_threshold})\n        return jsonify({'status': 'normal', 'threshold': computed_threshold})\n    except ValueError as e:\n        return jsonify({'error': f'Invalid threshold expression: {str(e)}'}), 400\n    except (ZeroDivisionError, OverflowError) as e:\n        return jsonify({'error': f'Mathematical error: {str(e)}'}), 400"}