{"title":"Arbitrary Module Execution via `importlib.import_module` on User-Controlled Name","language":"Python","severity":"Critical","cwe":"CWE-94","source_lines":[8,9],"flow_lines":[8,10,11],"sink_lines":[11],"vulnerable_code":"import importlib\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/api/cloud/invoke', methods=['POST'])\ndef invoke_cloud_service():\n    provider = request.json.get('provider', 'aws')\n    action = request.json.get('action', 'list_instances')\n    service_module = f\"cloud_adapters.{provider}.{action}\"\n    try:\n        mod = importlib.import_module(service_module)\n        result = mod.execute(request.json.get('params', {}))\n        return jsonify({'status': 'success', 'data': result})\n    except Exception as e:\n        return jsonify({'status': 'error', 'message': str(e)}), 500","explanation":"The application constructs a module path using unsanitized user input from 'provider' and 'action' JSON fields, then passes it directly to importlib.import_module(). An attacker can inject malicious module paths (e.g., '__main__', 'os', or arbitrary installed packages) to execute unintended code or access sensitive functionality.","remediation":"The fix implements a strict allowlist of valid provider and action combinations, rejecting any input that is not explicitly permitted before it reaches `importlib.import_module()`. Additionally, it validates that both values are valid Python identifiers to prevent path traversal or null-byte injection, and it avoids leaking internal error details to the client.","secure_code":"import importlib\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n# Allowlist of valid provider and action combinations\nALLOWED_PROVIDERS = {'aws', 'azure', 'gcp'}\nALLOWED_ACTIONS = {\n    'aws': {'list_instances', 'start_instance', 'stop_instance', 'list_buckets'},\n    'azure': {'list_vms', 'start_vm', 'stop_vm', 'list_storage'},\n    'gcp': {'list_instances', 'start_instance', 'stop_instance', 'list_buckets'}\n}\n\n\ndef validate_provider_action(provider, action):\n    \"\"\"Validate that provider and action are in the allowlist and contain only safe characters.\"\"\"\n    if provider not in ALLOWED_PROVIDERS:\n        return False, f\"Invalid provider: '{provider}'. Allowed: {sorted(ALLOWED_PROVIDERS)}\"\n    if provider not in ALLOWED_ACTIONS or action not in ALLOWED_ACTIONS[provider]:\n        return False, f\"Invalid action: '{action}' for provider '{provider}'. Allowed: {sorted(ALLOWED_ACTIONS.get(provider, set()))}\"\n    # Additional safety: ensure no path traversal or special characters\n    if not provider.isidentifier() or not action.isidentifier():\n        return False, \"Provider and action must be valid Python identifiers\"\n    return True, None\n\n\n@app.route('/api/cloud/invoke', methods=['POST'])\ndef invoke_cloud_service():\n    if not request.json:\n        return jsonify({'status': 'error', 'message': 'Request body must be JSON'}), 400\n\n    provider = request.json.get('provider', 'aws')\n    action = request.json.get('action', 'list_instances')\n\n    # Validate against allowlist before constructing module path\n    is_valid, error_message = validate_provider_action(provider, action)\n    if not is_valid:\n        return jsonify({'status': 'error', 'message': error_message}), 400\n\n    service_module = f\"cloud_adapters.{provider}.{action}\"\n    try:\n        mod = importlib.import_module(service_module)\n        result = mod.execute(request.json.get('params', {}))\n        return jsonify({'status': 'success', 'data': result})\n    except Exception as e:\n        return jsonify({'status': 'error', 'message': 'Internal error executing cloud action'}), 500"}