{"title":"Code Injection via eval() on Untrusted User Input","language":"Python","severity":"Critical","cwe":"CWE-95","source_lines":[9],"flow_lines":[9,11],"sink_lines":[11],"vulnerable_code":"def apply_iot_device_filter(sensor_data, filter_expression):\n    filtered_results = []\n    for device in sensor_data:\n        try:\n            if eval(filter_expression, {'device': device, '__builtins__': {}}):\n                filtered_results.append(device)\n        except:\n            pass\n    return filtered_results","explanation":"The function accepts untrusted user input via the filter_expression parameter and directly passes it to eval() on line 5. Despite attempting to restrict __builtins__, attackers can bypass this sandboxing through Python introspection and attribute access (e.g., accessing __class__.__bases__ chains) to achieve arbitrary code execution.","remediation":"The fix replaces the dangerous eval() call with a custom AST-based expression evaluator that parses the filter expression into an abstract syntax tree and only allows safe operations (comparisons, boolean logic, subscript access on dictionaries, and literal values). Function calls, attribute access, lambda expressions, comprehensions, and all other potentially dangerous constructs are explicitly rejected during AST validation before evaluation begins.","secure_code":"import ast\nimport operator\n\nSAFE_OPERATORS = {\n    ast.Eq: operator.eq,\n    ast.NotEq: operator.ne,\n    ast.Lt: operator.lt,\n    ast.LtE: operator.le,\n    ast.Gt: operator.gt,\n    ast.GtE: operator.ge,\n    ast.And: lambda *args: all(args),\n    ast.Or: lambda *args: any(args),\n    ast.Not: operator.not_,\n    ast.Add: operator.add,\n    ast.Sub: operator.sub,\n    ast.Mult: operator.mul,\n    ast.Div: operator.truediv,\n    ast.In: lambda a, b: a in b,\n    ast.NotIn: lambda a, b: a not in b,\n}\n\n\ndef _safe_eval_node(node, device):\n    \"\"\"Recursively evaluate an AST node with only safe operations allowed.\"\"\"\n    if isinstance(node, ast.Expression):\n        return _safe_eval_node(node.body, device)\n    elif isinstance(node, ast.BoolOp):\n        op = SAFE_OPERATORS.get(type(node.op))\n        if op is None:\n            raise ValueError(f\"Unsupported boolean operator: {type(node.op).__name__}\")\n        values = [_safe_eval_node(v, device) for v in node.values]\n        return op(*values)\n    elif isinstance(node, ast.UnaryOp):\n        if isinstance(node.op, ast.Not):\n            return not _safe_eval_node(node.operand, device)\n        raise ValueError(f\"Unsupported unary operator: {type(node.op).__name__}\")\n    elif isinstance(node, ast.BinOp):\n        op = SAFE_OPERATORS.get(type(node.op))\n        if op is None:\n            raise ValueError(f\"Unsupported binary operator: {type(node.op).__name__}\")\n        left = _safe_eval_node(node.left, device)\n        right = _safe_eval_node(node.right, device)\n        return op(left, right)\n    elif isinstance(node, ast.Compare):\n        left = _safe_eval_node(node.left, device)\n        for op_node, comparator in zip(node.ops, node.comparators):\n            op = SAFE_OPERATORS.get(type(op_node))\n            if op is None:\n                raise ValueError(f\"Unsupported comparison operator: {type(op_node).__name__}\")\n            right = _safe_eval_node(comparator, device)\n            if not op(left, right):\n                return False\n            left = right\n        return True\n    elif isinstance(node, ast.Subscript):\n        value = _safe_eval_node(node.value, device)\n        if not isinstance(value, dict):\n            raise ValueError(\"Subscript access is only allowed on device dictionaries\")\n        if isinstance(node.slice, ast.Constant):\n            key = node.slice.value\n        elif isinstance(node.slice, ast.Index):\n            key = _safe_eval_node(node.slice.value, device)\n        else:\n            raise ValueError(\"Only constant subscript keys are allowed\")\n        if not isinstance(key, (str, int)):\n            raise ValueError(\"Subscript key must be a string or integer\")\n        return value[key]\n    elif isinstance(node, ast.Name):\n        if node.id == 'device':\n            return device\n        elif node.id in ('True', 'true'):\n            return True\n        elif node.id in ('False', 'false'):\n            return False\n        elif node.id == 'None':\n            return None\n        raise ValueError(f\"Access to variable '{node.id}' is not allowed\")\n    elif isinstance(node, ast.Constant):\n        if isinstance(node.value, (int, float, str, bool, type(None))):\n            return node.value\n        raise ValueError(f\"Unsupported constant type: {type(node.value).__name__}\")\n    elif isinstance(node, ast.Num):\n        return node.n\n    elif isinstance(node, ast.Str):\n        return node.s\n    elif isinstance(node, ast.NameConstant):\n        return node.value\n    elif isinstance(node, ast.List):\n        return [_safe_eval_node(elt, device) for elt in node.elts]\n    elif isinstance(node, ast.Tuple):\n        return tuple(_safe_eval_node(elt, device) for elt in node.elts)\n    else:\n        raise ValueError(f\"Unsupported expression type: {type(node).__name__}\")\n\n\ndef safe_evaluate_filter(filter_expression, device):\n    \"\"\"Safely evaluate a filter expression against a device dictionary.\"\"\"\n    try:\n        tree = ast.parse(filter_expression, mode='eval')\n    except SyntaxError as e:\n        raise ValueError(f\"Invalid filter expression syntax: {e}\")\n\n    for node in ast.walk(tree):\n        if isinstance(node, (ast.Call, ast.Attribute, ast.Import, ast.ImportFrom,\n                             ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef,\n                             ast.Lambda, ast.GeneratorExp, ast.ListComp,\n                             ast.SetComp, ast.DictComp, ast.Await,\n                             ast.Yield, ast.YieldFrom, ast.Global,\n                             ast.Nonlocal, ast.Delete, ast.Assign,\n                             ast.AugAssign, ast.FormattedValue, ast.JoinedStr)):\n            raise ValueError(f\"Disallowed expression type: {type(node).__name__}. \"\n                             \"Only comparisons, boolean logic, and dictionary access are allowed.\")\n\n    return _safe_eval_node(tree, device)\n\n\ndef apply_iot_device_filter(sensor_data, filter_expression):\n    \"\"\"Filter IoT sensor data based on a safe filter expression.\n\n    Supported expressions:\n        - device[\"temperature\"] > 25\n        - device[\"status\"] == \"active\"\n        - device[\"temperature\"] > 20 and device[\"humidity\"] < 80\n        - device[\"type\"] in [\"sensor\", \"actuator\"]\n    \"\"\"\n    if not isinstance(filter_expression, str):\n        raise ValueError(\"Filter expression must be a string\")\n    if len(filter_expression) > 500:\n        raise ValueError(\"Filter expression is too long (max 500 characters)\")\n\n    filtered_results = []\n    for device in sensor_data:\n        try:\n            if safe_evaluate_filter(filter_expression, device):\n                filtered_results.append(device)\n        except (ValueError, KeyError, TypeError, IndexError):\n            pass\n    return filtered_results"}