{"title":"Arbitrary Code Execution via logging.config.fileConfig() User-Controlled Config Path","language":"Python","severity":"Critical","cwe":"CWE-94","source_lines":[10,11],"flow_lines":[10,11,12,13],"sink_lines":[13],"vulnerable_code":"from flask import Flask, request\nimport logging.config\nimport os\n\napp = Flask(__name__)\n\n@app.route('/iot/device/diagnostics', methods=['POST'])\ndef configure_device_logging():\n    device_id = request.json.get('device_id')\n    log_profile = request.json.get('log_profile', 'default.ini')\n    config_path = f\"/var/iot/devices/{device_id}/logging/{log_profile}\"\n    if os.path.exists(config_path):\n        logging.config.fileConfig(config_path)\n        return {\"status\": \"logging configured\", \"device\": device_id}, 200\n    return {\"error\": \"config not found\"}, 404","explanation":"The application accepts user-controlled input (device_id and log_profile) and constructs a file path without proper validation, then passes it to logging.config.fileConfig(). This function can execute arbitrary Python code embedded in malicious INI configuration files through class instantiation directives, leading to remote code execution.","remediation":"The fix addresses both path traversal and arbitrary code execution. It validates device_id and log_profile with strict regex allowlists (alphanumeric, hyphens, underscores only), resolves the final path and ensures it stays within the expected base directory, and validates that any handler/formatter classes referenced in the INI file are on a strict allowlist of safe logging classes before calling fileConfig().","secure_code":"from flask import Flask, request\nimport logging.config\nimport os\nimport re\n\napp = Flask(__name__)\n\nALLOWED_DEVICE_ID_PATTERN = re.compile(r'^[a-zA-Z0-9_\\-]+$')\nALLOWED_LOG_PROFILE_PATTERN = re.compile(r'^[a-zA-Z0-9_\\-]+\\.ini$')\nBASE_CONFIG_DIR = '/var/iot/devices'\n\n@app.route('/iot/device/diagnostics', methods=['POST'])\ndef configure_device_logging():\n    device_id = request.json.get('device_id')\n    log_profile = request.json.get('log_profile', 'default.ini')\n\n    # Validate device_id and log_profile against strict allowlists\n    if not device_id or not ALLOWED_DEVICE_ID_PATTERN.match(device_id):\n        return {\"error\": \"invalid device_id\"}, 400\n\n    if not ALLOWED_LOG_PROFILE_PATTERN.match(log_profile):\n        return {\"error\": \"invalid log_profile\"}, 400\n\n    # Construct path and resolve to prevent path traversal\n    config_path = os.path.realpath(os.path.join(BASE_CONFIG_DIR, device_id, 'logging', log_profile))\n\n    # Ensure resolved path is within the expected base directory\n    expected_prefix = os.path.realpath(BASE_CONFIG_DIR) + os.sep\n    if not config_path.startswith(expected_prefix):\n        return {\"error\": \"invalid config path\"}, 400\n\n    if not os.path.exists(config_path):\n        return {\"error\": \"config not found\"}, 404\n\n    # Use logging.config.fileConfig with verify=True (Python 3.12+) or\n    # switch to dictConfig with a parsed and validated configuration\n    # to prevent arbitrary code execution via class= directives.\n    import configparser\n\n    ALLOWED_HANDLER_CLASSES = {\n        'logging.StreamHandler',\n        'logging.FileHandler',\n        'logging.handlers.RotatingFileHandler',\n        'logging.handlers.TimedRotatingFileHandler',\n        'logging.handlers.SysLogHandler',\n    }\n\n    ALLOWED_FORMATTER_CLASSES = {\n        'logging.Formatter',\n    }\n\n    config = configparser.ConfigParser()\n    config.read(config_path)\n\n    # Validate all handler class references\n    for section in config.sections():\n        if section.startswith('handler_'):\n            handler_class = config.get(section, 'class', fallback='')\n            if handler_class and handler_class not in ALLOWED_HANDLER_CLASSES:\n                return {\"error\": f\"disallowed handler class: {handler_class}\"}, 400\n        if section.startswith('formatter_'):\n            formatter_class = config.get(section, 'class', fallback='')\n            if formatter_class and formatter_class not in ALLOWED_FORMATTER_CLASSES:\n                return {\"error\": f\"disallowed formatter class: {formatter_class}\"}, 400\n\n    logging.config.fileConfig(config_path, disable_existing_loggers=False)\n    return {\"status\": \"logging configured\", \"device\": device_id}, 200"}