{"title":"Code Injection via exec() on User-Controlled Expression","language":"Python","severity":"Critical","cwe":"CWE-94","source_lines":[4],"flow_lines":[4,7],"sink_lines":[7],"vulnerable_code":"@app.route('/iot/device/calibrate', methods=['POST'])\ndef calibrate_sensor():\n    device_id = request.json.get('device_id')\n    calibration_formula = request.json.get('formula', 'value * 1.0')\n    sensor_value = float(request.json.get('raw_value', 0))\n    result_var = {}\n    exec(f\"calibrated = {calibration_formula}\", {'value': sensor_value, '__builtins__': {}}, result_var)\n    db.execute(\"UPDATE iot_sensors SET calibrated_value = ? WHERE id = ?\", (result_var.get('calibrated'), device_id))\n    return jsonify({'device': device_id, 'calibrated_value': result_var.get('calibrated')})","explanation":"The calibration_formula parameter from user input is directly interpolated into an exec() call without proper sanitization. Although __builtins__ is restricted to an empty dict, attackers can still access dangerous functions through existing objects in the namespace or use attribute access to execute arbitrary code.","remediation":"The fix replaces the dangerous exec() call with a safe AST-based mathematical expression evaluator that only allows arithmetic operations, specific safe functions (like sqrt, log, sin), and named variables. The evaluator parses the expression into an AST and walks each node, rejecting any construct that isn't a number, allowed variable, safe operator, or whitelisted function call—completely preventing code injection, attribute access, and import statements.","secure_code":"import ast\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.Pow: operator.pow,\n    ast.USub: operator.neg,\n    ast.UAdd: operator.pos,\n    ast.Mod: operator.mod,\n    ast.FloorDiv: operator.floordiv,\n}\n\nSAFE_FUNCTIONS = {\n    'abs': abs,\n    'round': round,\n    'min': min,\n    'max': max,\n    'int': int,\n    'float': float,\n}\n\nimport math\nSAFE_CONSTANTS = {\n    'pi': math.pi,\n    'e': math.e,\n}\n\nSAFE_MATH_FUNCTIONS = {\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}\n\nSAFE_FUNCTIONS.update(SAFE_MATH_FUNCTIONS)\n\n\ndef safe_eval_expr(expr_str, variables):\n    \"\"\"Safely evaluate a mathematical expression with given variables.\"\"\"\n    try:\n        tree = ast.parse(expr_str, mode='eval')\n    except SyntaxError:\n        raise ValueError(f\"Invalid expression syntax: {expr_str}\")\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    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    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        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        if isinstance(node.func, ast.Name) and node.func.id in SAFE_FUNCTIONS:\n            args = [_eval_node(arg, variables) for arg in node.args]\n            if node.keywords:\n                raise ValueError(\"Keyword arguments not supported\")\n            return SAFE_FUNCTIONS[node.func.id](*args)\n        raise ValueError(f\"Unsupported function call\")\n    elif isinstance(node, ast.IfExp):\n        raise ValueError(\"Conditional expressions not supported\")\n    elif isinstance(node, ast.Compare):\n        raise ValueError(\"Comparison expressions not supported in calibration formulas\")\n    elif isinstance(node, ast.Attribute):\n        raise ValueError(\"Attribute access not allowed\")\n    else:\n        raise ValueError(f\"Unsupported expression type: {type(node).__name__}\")\n\n\n@app.route('/iot/device/calibrate', methods=['POST'])\ndef calibrate_sensor():\n    device_id = request.json.get('device_id')\n    calibration_formula = request.json.get('formula', 'value * 1.0')\n    sensor_value = float(request.json.get('raw_value', 0))\n\n    if len(calibration_formula) > 200:\n        return jsonify({'error': 'Formula too long'}), 400\n\n    try:\n        calibrated = safe_eval_expr(calibration_formula, {'value': sensor_value})\n    except (ValueError, TypeError, ZeroDivisionError, OverflowError) as e:\n        return jsonify({'error': f'Invalid calibration formula: {str(e)}'}), 400\n\n    db.execute(\"UPDATE iot_sensors SET calibrated_value = ? WHERE id = ?\", (calibrated, device_id))\n    return jsonify({'device': device_id, 'calibrated_value': calibrated})"}