{"title":"Code Injection via exec on Untrusted Input","language":"Python","severity":"Critical","cwe":"CWE-94","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    config_expr = request.json.get('threshold_logic')\n    device_state = {'temp': 25, 'humidity': 60, 'device_id': device_id}\n    try:\n        exec(f\"alert_triggered = {config_expr}\", {'__builtins__': {}}, device_state)\n        return jsonify({'device': device_id, 'alert': device_state.get('alert_triggered', False)})\n    except Exception as e:\n        return jsonify({'error': 'Configuration failed'}), 400","explanation":"User-controlled input from 'threshold_logic' is directly passed to exec() without proper sanitization. Although __builtins__ is restricted, attackers can still manipulate the device_state namespace to execute arbitrary Python code, access object methods, or perform denial of service attacks through the local namespace.","remediation":"The fix replaces the dangerous exec() call with a custom AST-based expression evaluator that only allows safe operations: numeric comparisons (>, <, >=, <=, ==, !=), boolean operators (and, or), numeric constants, and a whitelist of allowed variable names (temp, humidity). This prevents any arbitrary code execution while still supporting legitimate threshold expressions like 'temp > 30 and humidity < 80'.","secure_code":"import ast\nimport operator\n\nALLOWED_OPERATORS = {\n    ast.Lt: operator.lt,\n    ast.Gt: operator.gt,\n    ast.LtE: operator.le,\n    ast.GtE: operator.ge,\n    ast.Eq: operator.eq,\n    ast.NotEq: operator.ne,\n}\n\nALLOWED_BOOL_OPS = {\n    ast.And: all,\n    ast.Or: any,\n}\n\nALLOWED_VARIABLES = {'temp', 'humidity'}\n\n\ndef safe_eval_threshold(expr, device_state):\n    \"\"\"Safely evaluate a threshold expression using AST parsing.\n    Only allows comparisons, boolean operations, numbers, and whitelisted variables.\"\"\"\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, device_state)\n\n\ndef _eval_node(node, device_state):\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.Name):\n        if node.id in ALLOWED_VARIABLES:\n            return device_state[node.id]\n        raise ValueError(f\"Disallowed variable: {node.id}\")\n\n    elif isinstance(node, ast.Compare):\n        left = _eval_node(node.left, device_state)\n        for op, comparator in zip(node.ops, node.comparators):\n            op_func = ALLOWED_OPERATORS.get(type(op))\n            if op_func is None:\n                raise ValueError(f\"Unsupported operator: {type(op).__name__}\")\n            right = _eval_node(comparator, device_state)\n            if not op_func(left, right):\n                return False\n            left = right\n        return True\n\n    elif isinstance(node, ast.BoolOp):\n        bool_func = ALLOWED_BOOL_OPS.get(type(node.op))\n        if bool_func is None:\n            raise ValueError(f\"Unsupported boolean operator: {type(node.op).__name__}\")\n        values = [_eval_node(v, device_state) for v in node.values]\n        return bool_func(values)\n\n    elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):\n        operand = _eval_node(node.operand, device_state)\n        if isinstance(operand, (int, float)):\n            return -operand\n        raise ValueError(\"Unary minus only allowed on numbers\")\n\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    config_expr = request.json.get('threshold_logic')\n\n    if not isinstance(config_expr, str) or len(config_expr) > 200:\n        return jsonify({'error': 'Invalid or too long threshold expression'}), 400\n\n    device_state = {'temp': 25, 'humidity': 60, 'device_id': device_id}\n    try:\n        alert_triggered = safe_eval_threshold(config_expr, device_state)\n        return jsonify({'device': device_id, 'alert': bool(alert_triggered)})\n    except (ValueError, KeyError) as e:\n        return jsonify({'error': 'Configuration failed'}), 400"}