{"title":"Code Injection via unsafe eval()","language":"Python","severity":"Critical","cwe":"CWE-94","source_lines":[5],"flow_lines":[5,6,7,8],"sink_lines":[8],"vulnerable_code":"from flask import Flask, request, jsonify\napp = Flask(__name__)\ndevice_registry = {'sensor_01': 45.2, 'sensor_02': 78.9, 'actuator_03': 1}\ndef evaluate_iot_threshold(sensor_id):\n    raw_expr = request.args.get('threshold_rule', '')\n    current_val = device_registry.get(sensor_id, 0)\n    ctx = {'value': current_val, 'sensor': sensor_id}\n    result = eval(raw_expr, {'__builtins__': __builtins__}, ctx)\n    return jsonify({'sensor': sensor_id, 'triggered': bool(result)})\n@app.route('/iot/check/<sensor_id>')\ndef iot_check(sensor_id):\n    return evaluate_iot_threshold(sensor_id)","explanation":"The `threshold_rule` query parameter is fetched directly from user-controlled HTTP input on line 5 without any sanitization or validation. It is then passed as the expression argument to `eval()` on line 8 with `__builtins__` explicitly exposed, meaning any Python code—including OS commands via `__import__`—can be executed server-side. Despite the appearance of a sandboxed context dict, passing `__builtins__: __builtins__` (the real builtins module) completely negates any sandboxing intent.","remediation":"The dangerous eval() call has been completely replaced with a custom AST-based expression evaluator that only permits comparisons, basic arithmetic, boolean logic, numeric constants, and a whitelisted set of variable names (only 'value'). The expression is parsed into an AST tree and each node is validated against an allowlist of safe node types, making it impossible to call functions, import modules, or access arbitrary attributes. Additionally, input length is bounded, sensor_id is validated against the registry, and errors are returned as 400 responses.","secure_code":"from flask import Flask, request, jsonify\nimport ast\nimport operator\n\napp = Flask(__name__)\ndevice_registry = {'sensor_01': 45.2, 'sensor_02': 78.9, 'actuator_03': 1}\n\n# Safe operators for threshold evaluation\n_SAFE_OPS = {\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    ast.Add: operator.add,\n    ast.Sub: operator.sub,\n    ast.Mult: operator.mul,\n    ast.Div: operator.truediv,\n    ast.And: None,\n    ast.Or: None,\n    ast.Not: None,\n}\n\n\ndef _safe_eval_node(node, ctx):\n    \"\"\"Recursively evaluate a restricted AST node against a safe context.\"\"\"\n    if isinstance(node, ast.Expression):\n        return _safe_eval_node(node.body, ctx)\n    elif isinstance(node, ast.Constant):  # numbers, strings, booleans\n        if isinstance(node.value, (int, float, bool)):\n            return node.value\n        raise ValueError(f\"Unsupported constant type: {type(node.value).__name__}\")\n    elif isinstance(node, ast.Name):\n        if node.id in ctx:\n            return ctx[node.id]\n        raise ValueError(f\"Unknown variable: {node.id}\")\n    elif isinstance(node, ast.Compare):\n        left = _safe_eval_node(node.left, ctx)\n        for op_node, comparator in zip(node.ops, node.comparators):\n            op_func = _SAFE_OPS.get(type(op_node))\n            if op_func is None:\n                raise ValueError(f\"Unsupported comparison operator: {type(op_node).__name__}\")\n            right = _safe_eval_node(comparator, ctx)\n            if not op_func(left, right):\n                return False\n            left = right\n        return True\n    elif isinstance(node, ast.BinOp):\n        op_func = _SAFE_OPS.get(type(node.op))\n        if op_func is None:\n            raise ValueError(f\"Unsupported binary operator: {type(node.op).__name__}\")\n        left = _safe_eval_node(node.left, ctx)\n        right = _safe_eval_node(node.right, ctx)\n        return op_func(left, right)\n    elif isinstance(node, ast.BoolOp):\n        if isinstance(node.op, ast.And):\n            return all(_safe_eval_node(v, ctx) for v in node.values)\n        elif isinstance(node.op, ast.Or):\n            return any(_safe_eval_node(v, ctx) for v in node.values)\n        raise ValueError(f\"Unsupported boolean operator: {type(node.op).__name__}\")\n    elif isinstance(node, ast.UnaryOp):\n        if isinstance(node.op, ast.Not):\n            return not _safe_eval_node(node.operand, ctx)\n        elif isinstance(node.op, ast.USub):\n            return -_safe_eval_node(node.operand, ctx)\n        raise ValueError(f\"Unsupported unary operator: {type(node.op).__name__}\")\n    else:\n        raise ValueError(f\"Unsupported expression node: {type(node).__name__}\")\n\n\ndef safe_eval_threshold(expression, ctx):\n    \"\"\"Parse and evaluate a threshold expression using a restricted AST walker.\n    Only comparisons, arithmetic, boolean logic, numbers, and whitelisted names are allowed.\n    \"\"\"\n    if not expression or len(expression) > 200:\n        raise ValueError(\"Invalid or excessively long threshold expression\")\n    try:\n        tree = ast.parse(expression, mode='eval')\n    except SyntaxError as e:\n        raise ValueError(f\"Invalid expression syntax: {e}\")\n    return _safe_eval_node(tree, ctx)\n\n\ndef evaluate_iot_threshold(sensor_id):\n    raw_expr = request.args.get('threshold_rule', '')\n    current_val = device_registry.get(sensor_id, 0)\n    ctx = {'value': current_val}\n    try:\n        result = safe_eval_threshold(raw_expr, ctx)\n    except ValueError as e:\n        return jsonify({'error': str(e)}), 400\n    return jsonify({'sensor': sensor_id, 'triggered': bool(result)})\n\n\n@app.route('/iot/check/<sensor_id>')\ndef iot_check(sensor_id):\n    if sensor_id not in device_registry:\n        return jsonify({'error': 'Unknown sensor'}), 404\n    return evaluate_iot_threshold(sensor_id)"}