# Arbitrary Code Execution via logging.config.fileConfig() User-Controlled Config Path

Language: Python
Severity: Critical
CWE: CWE-94

## Source
10-11

## Flow
10-11-12-13

## Sink
13

## Vulnerable Code
```python
from flask import Flask, request
import logging.config
import os

app = Flask(__name__)

@app.route('/iot/device/diagnostics', methods=['POST'])
def configure_device_logging():
    device_id = request.json.get('device_id')
    log_profile = request.json.get('log_profile', 'default.ini')
    config_path = f"/var/iot/devices/{device_id}/logging/{log_profile}"
    if os.path.exists(config_path):
        logging.config.fileConfig(config_path)
        return {"status": "logging configured", "device": device_id}, 200
    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
```python
from flask import Flask, request
import logging.config
import os
import re

app = Flask(__name__)

ALLOWED_DEVICE_ID_PATTERN = re.compile(r'^[a-zA-Z0-9_\-]+$')
ALLOWED_LOG_PROFILE_PATTERN = re.compile(r'^[a-zA-Z0-9_\-]+\.ini$')
BASE_CONFIG_DIR = '/var/iot/devices'

@app.route('/iot/device/diagnostics', methods=['POST'])
def configure_device_logging():
    device_id = request.json.get('device_id')
    log_profile = request.json.get('log_profile', 'default.ini')

    # Validate device_id and log_profile against strict allowlists
    if not device_id or not ALLOWED_DEVICE_ID_PATTERN.match(device_id):
        return {"error": "invalid device_id"}, 400

    if not ALLOWED_LOG_PROFILE_PATTERN.match(log_profile):
        return {"error": "invalid log_profile"}, 400

    # Construct path and resolve to prevent path traversal
    config_path = os.path.realpath(os.path.join(BASE_CONFIG_DIR, device_id, 'logging', log_profile))

    # Ensure resolved path is within the expected base directory
    expected_prefix = os.path.realpath(BASE_CONFIG_DIR) + os.sep
    if not config_path.startswith(expected_prefix):
        return {"error": "invalid config path"}, 400

    if not os.path.exists(config_path):
        return {"error": "config not found"}, 404

    # Use logging.config.fileConfig with verify=True (Python 3.12+) or
    # switch to dictConfig with a parsed and validated configuration
    # to prevent arbitrary code execution via class= directives.
    import configparser

    ALLOWED_HANDLER_CLASSES = {
        'logging.StreamHandler',
        'logging.FileHandler',
        'logging.handlers.RotatingFileHandler',
        'logging.handlers.TimedRotatingFileHandler',
        'logging.handlers.SysLogHandler',
    }

    ALLOWED_FORMATTER_CLASSES = {
        'logging.Formatter',
    }

    config = configparser.ConfigParser()
    config.read(config_path)

    # Validate all handler class references
    for section in config.sections():
        if section.startswith('handler_'):
            handler_class = config.get(section, 'class', fallback='')
            if handler_class and handler_class not in ALLOWED_HANDLER_CLASSES:
                return {"error": f"disallowed handler class: {handler_class}"}, 400
        if section.startswith('formatter_'):
            formatter_class = config.get(section, 'class', fallback='')
            if formatter_class and formatter_class not in ALLOWED_FORMATTER_CLASSES:
                return {"error": f"disallowed formatter class: {formatter_class}"}, 400

    logging.config.fileConfig(config_path, disable_existing_loggers=False)
    return {"status": "logging configured", "device": device_id}, 200
```
