{"title":"Code Injection via eval()","language":"Python","severity":"Critical","cwe":"CWE-94","source_lines":[9],"flow_lines":[9,11],"sink_lines":[11],"vulnerable_code":"from flask import Flask, request, jsonify\napp = Flask(__name__)\nsensor_limits = {}\n@app.route('/iot/threshold', methods=['POST'])\ndef configure_threshold():\n    dev_id = request.json.get('device_id')\n    raw_expr = request.json.get('threshold_formula')\n    try:\n        computed_limit = eval(raw_expr)\n        sensor_limits[dev_id] = computed_limit\n        return jsonify({'status': 'ok', 'limit': computed_limit})\n    except Exception as e:\n        return jsonify({'error': str(e)}), 400","explanation":"The `threshold_formula` field is read directly from untrusted user-supplied JSON and passed without any sanitization or validation to Python's built-in `eval()`. This allows an attacker to inject and execute arbitrary Python code on the server, including OS commands, file reads, or spawning reverse shells.","remediation":"The dangerous `eval()` call has been replaced with a custom AST-based safe expression evaluator (`safe_eval_expr`) that parses the formula into an abstract syntax tree and only allows numeric literals, pre-approved variable names, and basic arithmetic operators (+, -, *, /, //, %, **). Any attempt to inject function calls, attribute access, imports, or other arbitrary Python code will be rejected with a ValueError. Input validation also ensures the formula is a non-empty string and any supplied variables map only strings to numbers.","secure_code":"from flask import Flask, request, jsonify\nimport ast\nimport operator\n\napp = Flask(__name__)\nsensor_limits = {}\n\n# Safe mathematical expression evaluator\n_ALLOWED_OPS = {\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\ndef safe_eval_expr(expr_str, variables=None):\n    \"\"\"Safely evaluate a mathematical expression containing only numbers,\n    basic arithmetic operators, and optionally pre-defined variable names.\"\"\"\n    if variables is None:\n        variables = {}\n\n    def _eval_node(node):\n        if isinstance(node, ast.Expression):\n            return _eval_node(node.body)\n        elif isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):\n            return node.value\n        elif isinstance(node, ast.Name) and node.id in variables:\n            return variables[node.id]\n        elif isinstance(node, ast.BinOp):\n            op_type = type(node.op)\n            if op_type not in _ALLOWED_OPS:\n                raise ValueError(f\"Unsupported operator: {op_type.__name__}\")\n            left = _eval_node(node.left)\n            right = _eval_node(node.right)\n            return _ALLOWED_OPS[op_type](left, right)\n        elif isinstance(node, ast.UnaryOp):\n            op_type = type(node.op)\n            if op_type not in _ALLOWED_OPS:\n                raise ValueError(f\"Unsupported unary operator: {op_type.__name__}\")\n            operand = _eval_node(node.operand)\n            return _ALLOWED_OPS[op_type](operand)\n        else:\n            raise ValueError(f\"Unsupported expression element: {ast.dump(node)}\")\n\n    tree = ast.parse(expr_str.strip(), mode='eval')\n    return _eval_node(tree)\n\n\n@app.route('/iot/threshold', methods=['POST'])\ndef configure_threshold():\n    dev_id = request.json.get('device_id')\n    raw_expr = request.json.get('threshold_formula')\n\n    if not isinstance(raw_expr, str) or not raw_expr.strip():\n        return jsonify({'error': 'threshold_formula must be a non-empty string'}), 400\n\n    # Optional: allow operators to reference named constants in the formula\n    allowed_variables = request.json.get('variables', {})\n    if not isinstance(allowed_variables, dict) or not all(\n        isinstance(k, str) and isinstance(v, (int, float))\n        for k, v in allowed_variables.items()\n    ):\n        return jsonify({'error': 'variables must be a dict of string->number'}), 400\n\n    try:\n        computed_limit = safe_eval_expr(raw_expr, variables=allowed_variables)\n        sensor_limits[dev_id] = computed_limit\n        return jsonify({'status': 'ok', 'limit': computed_limit})\n    except Exception as e:\n        return jsonify({'error': str(e)}), 400"}