{"title":"Logging Configuration Injection via logging.config.listen()","language":"Python","severity":"Critical","cwe":"CWE-502","source_lines":[4],"flow_lines":[4,6,7],"sink_lines":[7],"vulnerable_code":"import logging.config\nimport threading\n\ndef bootstrap_cloud_telemetry(metrics_port, enable_remote=True):\n    if enable_remote:\n        telemetry_host = '0.0.0.0'\n        config_listener = threading.Thread(target=logging.config.listen, args=(metrics_port,), daemon=False)\n        config_listener.start()\n        print(f\"[CloudMetrics] Telemetry listener active on {telemetry_host}:{metrics_port}\")\n        return config_listener\n    return None\n\nbootstrap_cloud_telemetry(9030)","explanation":"The code accepts an untrusted metrics_port parameter and passes it directly to logging.config.listen(), which deserializes pickle data from network connections. An attacker can send malicious pickled objects to execute arbitrary code on the server since pickle deserialization is inherently unsafe with untrusted input.","remediation":"The fix eliminates the use of logging.config.listen() which deserializes arbitrary pickle data from the network. Instead, it implements a custom listener that only accepts JSON-formatted configurations, validates them against a strict allowlist of keys and dangerous patterns, and applies them safely via logging.config.dictConfig(). The listener is also bound to localhost only (127.0.0.1) instead of all interfaces, and includes connection source validation and port input validation.","secure_code":"import logging.config\nimport json\nimport socket\nimport threading\n\nALLOWED_LOG_LEVELS = {'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'}\nALLOWED_HOSTS = {'127.0.0.1', '::1'}\n\ndef verify_logging_config(config_text):\n    \"\"\"Validate and sanitize logging configuration before applying.\"\"\"\n    try:\n        config = json.loads(config_text)\n    except (json.JSONDecodeError, TypeError):\n        logging.warning(\"[CloudMetrics] Rejected invalid JSON configuration\")\n        return None\n\n    # Validate structure - only allow known safe keys\n    allowed_keys = {'version', 'disable_existing_loggers', 'formatters', 'handlers', 'loggers', 'root'}\n    if not isinstance(config, dict):\n        return None\n    if not all(key in allowed_keys for key in config.keys()):\n        logging.warning(\"[CloudMetrics] Rejected config with unknown keys\")\n        return None\n\n    # Ensure no class references that could execute arbitrary code\n    config_str = json.dumps(config)\n    dangerous_patterns = ['()', 'eval', 'exec', 'import', '__', 'os.', 'subprocess', 'sys.']\n    for pattern in dangerous_patterns:\n        if pattern in config_str:\n            logging.warning(f\"[CloudMetrics] Rejected config containing dangerous pattern: {pattern}\")\n            return None\n\n    return config\n\n\ndef _config_listener_thread(port, bind_address='127.0.0.1'):\n    \"\"\"Safe configuration listener that accepts only JSON configs on localhost.\"\"\"\n    server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n    server_sock.settimeout(1.0)\n    server_sock.bind((bind_address, port))\n    server_sock.listen(1)\n    logging.info(f\"[CloudMetrics] Secure config listener on {bind_address}:{port}\")\n\n    while True:\n        try:\n            conn, addr = server_sock.accept()\n        except socket.timeout:\n            continue\n        except OSError:\n            break\n\n        # Only accept connections from allowed hosts\n        if addr[0] not in ALLOWED_HOSTS:\n            logging.warning(f\"[CloudMetrics] Rejected connection from unauthorized host: {addr[0]}\")\n            conn.close()\n            continue\n\n        try:\n            conn.settimeout(5.0)\n            # Read length-prefixed data (max 64KB)\n            data = b''\n            while len(data) < 65536:\n                chunk = conn.recv(4096)\n                if not chunk:\n                    break\n                data += chunk\n\n            config_text = data.decode('utf-8')\n            config = verify_logging_config(config_text)\n\n            if config is not None:\n                logging.config.dictConfig(config)\n                logging.info(\"[CloudMetrics] Logging configuration updated successfully\")\n            else:\n                logging.warning(\"[CloudMetrics] Configuration rejected by validation\")\n        except (UnicodeDecodeError, socket.timeout, ConnectionResetError) as e:\n            logging.warning(f\"[CloudMetrics] Error processing config update: {e}\")\n        finally:\n            conn.close()\n\n\ndef bootstrap_cloud_telemetry(metrics_port, enable_remote=True):\n    # Validate port\n    if not isinstance(metrics_port, int) or not (1024 <= metrics_port <= 65535):\n        raise ValueError(\"metrics_port must be an integer between 1024 and 65535\")\n\n    if enable_remote:\n        bind_address = '127.0.0.1'  # Only bind to localhost, not 0.0.0.0\n        config_listener = threading.Thread(\n            target=_config_listener_thread,\n            args=(metrics_port, bind_address),\n            daemon=True\n        )\n        config_listener.start()\n        print(f\"[CloudMetrics] Telemetry listener active on {bind_address}:{metrics_port}\")\n        return config_listener\n    return None\n\nbootstrap_cloud_telemetry(9030)"}