{"title":"Code Injection via eval() on Untrusted Input","language":"Python","severity":"Critical","cwe":"CWE-94","source_lines":[4],"flow_lines":[4,7],"sink_lines":[7],"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['temperature'] > computed_threshold:\n            trigger_alert(device_id)\n        return jsonify({'status': 'configured', 'threshold': computed_threshold})\n    except Exception as e:\n        return jsonify({'error': str(e)}), 400","explanation":"The application accepts user-controlled input 'threshold_expression' from a POST request and directly passes it to eval() without proper sanitization. Although a restricted namespace is provided, the custom globals dict does not set '__builtins__' to None, meaning Python's built-in functions remain accessible. This allows attackers to execute arbitrary Python code by leveraging builtins or object introspection, potentially compromising the entire system.","remediation":"The fix replaces the dangerous eval() call with a custom AST-based safe expression evaluator that only allows arithmetic operations, a whitelist of math functions, and access to predefined sensor variables. The parser walks the AST tree node by node, rejecting any constructs that are not explicitly permitted, making it impossible to execute arbitrary code. Additional input validation limits expression length, ensures required fields are present, and verifies the result is numeric.","secure_code":"import ast\nimport math\nimport operator\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    'abs': abs,\n    'round': round,\n    'min': min,\n    'max': max,\n    'sqrt': math.sqrt,\n    'log': math.log,\n    'log10': math.log10,\n    'ceil': math.ceil,\n    'floor': math.floor,\n    'sin': math.sin,\n    'cos': math.cos,\n    'tan': math.tan,\n    'pi': math.pi,\n    'e': math.e,\n}\n\n\ndef safe_eval_expr(expr, variables):\n    \"\"\"Safely evaluate a mathematical expression with given variables.\"\"\"\n    try:\n        tree = ast.parse(expr, mode='eval')\n    except SyntaxError:\n        raise ValueError(\"Invalid expression syntax\")\n    return _eval_node(tree.body, variables)\n\n\ndef _eval_node(node, variables):\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        name = node.id\n        if name in variables:\n            return variables[name]\n        if name in SAFE_MATH_FUNCTIONS:\n            return SAFE_MATH_FUNCTIONS[name]\n        raise ValueError(f\"Unknown variable or function: {name}\")\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        func = _eval_node(node.func, variables)\n        if func not in SAFE_MATH_FUNCTIONS.values():\n            raise ValueError(\"Unsupported function call\")\n        args = [_eval_node(arg, variables) for arg in node.args]\n        if node.keywords:\n            raise ValueError(\"Keyword arguments not supported\")\n        return func(*args)\n    elif isinstance(node, ast.Attribute):\n        value = _eval_node(node.value, variables)\n        if isinstance(value, dict):\n            attr = node.attr\n            if attr in value:\n                return value[attr]\n            raise ValueError(f\"Unknown attribute: {attr}\")\n        raise ValueError(\"Attribute access not allowed on non-dict objects\")\n    elif isinstance(node, ast.Subscript):\n        value = _eval_node(node.value, variables)\n        if isinstance(value, dict):\n            if isinstance(node.slice, ast.Constant):\n                key = node.slice.value\n                if key in value:\n                    return value[key]\n            raise ValueError(\"Invalid subscript access\")\n        raise ValueError(\"Subscript access not allowed on non-dict objects\")\n    else:\n        raise ValueError(f\"Unsupported expression type: {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': 'device_id and threshold_expression are required'}), 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\n    try:\n        variables = {\n            'sensor': sensor_data,\n            'temperature': sensor_data.get('temperature', 0),\n            'humidity': sensor_data.get('humidity', 0),\n            'pressure': sensor_data.get('pressure', 0),\n        }\n        computed_threshold = safe_eval_expr(threshold_expr, variables)\n\n        if not isinstance(computed_threshold, (int, float)):\n            return jsonify({'error': 'Expression must evaluate to a number'}), 400\n\n        if sensor_data['temperature'] > computed_threshold:\n            trigger_alert(device_id)\n        return jsonify({'status': 'configured', 'threshold': computed_threshold})\n    except ValueError as e:\n        return jsonify({'error': str(e)}), 400\n    except Exception as e:\n        return jsonify({'error': 'Invalid expression'}), 400"}