{"title":"Code Injection via eval() on Untrusted Expression Input","language":"Python","severity":"Critical","cwe":"CWE-94","source_lines":[3],"flow_lines":[3,9],"sink_lines":[9],"vulnerable_code":"@app.route('/iot/device/metrics', methods=['POST'])\ndef calculate_device_metrics():\n    device_id = request.json.get('device_id')\n    metric_formula = request.json.get('formula')\n    sensor_temp = float(request.json.get('temp', 0))\n    sensor_humidity = float(request.json.get('humidity', 0))\n    sensor_pressure = float(request.json.get('pressure', 0))\n    context = {'temp': sensor_temp, 'humidity': sensor_humidity, 'pressure': sensor_pressure, 'math': __import__('math')}\n    try:\n        computed_value = eval(metric_formula, context)\n        db.store_metric(device_id, computed_value)\n        return jsonify({'device': device_id, 'result': computed_value})\n    except Exception as e:\n        return jsonify({'error': 'Calculation failed'}), 400","explanation":"The application accepts a user-controlled 'formula' parameter and directly passes it to eval() without any validation or sanitization. Even though a restricted context dictionary is provided, eval() can still access dangerous built-in functions and methods that allow arbitrary code execution, bypassing the intended restrictions.","remediation":"The fix replaces the dangerous eval() call with a custom AST-based expression parser that walks the parsed syntax tree and only allows whitelisted operations (arithmetic operators, safe math functions, and declared sensor variables). This approach completely prevents code injection because arbitrary Python code, attribute access, imports, and other dangerous constructs are rejected at the AST level before any execution occurs.","secure_code":"import ast\nimport operator\nimport math\n\n# Safe mathematical operations whitelist\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_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    'sin': math.sin,\n    'cos': math.cos,\n    'tan': math.tan,\n    'pow': math.pow,\n    'exp': math.exp,\n    'ceil': math.ceil,\n    'floor': math.floor,\n}\n\nSAFE_CONSTANTS = {\n    'pi': math.pi,\n    'e': math.e,\n}\n\n\ndef safe_eval_formula(formula, variables):\n    \"\"\"Safely evaluate a mathematical formula using AST parsing.\"\"\"\n    try:\n        tree = ast.parse(formula, mode='eval')\n    except SyntaxError:\n        raise ValueError(\"Invalid formula syntax\")\n\n    return _eval_node(tree.body, variables)\n\n\ndef _eval_node(node, variables):\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 variables:\n            return variables[node.id]\n        if node.id in SAFE_CONSTANTS:\n            return SAFE_CONSTANTS[node.id]\n        raise ValueError(f\"Unknown variable: {node.id}\")\n\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\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\n    elif isinstance(node, ast.Call):\n        if not isinstance(node.func, ast.Name):\n            raise ValueError(\"Only simple function calls are allowed\")\n        func_name = node.func.id\n        if func_name not in SAFE_FUNCTIONS:\n            raise ValueError(f\"Unsupported function: {func_name}\")\n        args = [_eval_node(arg, variables) for arg in node.args]\n        if node.keywords:\n            raise ValueError(\"Keyword arguments are not allowed\")\n        return SAFE_FUNCTIONS[func_name](*args)\n\n    else:\n        raise ValueError(f\"Unsupported expression type: {type(node).__name__}\")\n\n\n@app.route('/iot/device/metrics', methods=['POST'])\ndef calculate_device_metrics():\n    device_id = request.json.get('device_id')\n    metric_formula = request.json.get('formula')\n\n    if not metric_formula or not isinstance(metric_formula, str):\n        return jsonify({'error': 'Invalid formula'}), 400\n\n    if len(metric_formula) > 500:\n        return jsonify({'error': 'Formula too long'}), 400\n\n    sensor_temp = float(request.json.get('temp', 0))\n    sensor_humidity = float(request.json.get('humidity', 0))\n    sensor_pressure = float(request.json.get('pressure', 0))\n\n    variables = {'temp': sensor_temp, 'humidity': sensor_humidity, 'pressure': sensor_pressure}\n\n    try:\n        computed_value = safe_eval_formula(metric_formula, variables)\n        db.store_metric(device_id, computed_value)\n        return jsonify({'device': device_id, 'result': computed_value})\n    except (ValueError, TypeError, ZeroDivisionError, OverflowError) as e:\n        return jsonify({'error': 'Calculation failed'}), 400"}