# Arbitrary File Read via Unvalidated User-Supplied File Path

Language: Python
Severity: Critical
CWE: CWE-22

## Source
3-4

## Flow
3-5-6

## Sink
6

## Vulnerable Code
```python
@app.route('/api/v2/iot/device/logs', methods=['GET'])
def fetch_device_logs():
    device_id = request.args.get('device_id')
    log_type = request.args.get('log_type', 'system')
    log_path = f"/var/iot/devices/{device_id}/logs/{log_type}.log"
    try:
        with open(log_path, 'r') as log_file:
            log_content = log_file.read()
        return jsonify({"device": device_id, "logs": log_content, "status": "success"}), 200
    except FileNotFoundError:
        return jsonify({"error": "Log file not found"}), 404
    except Exception as e:
        return jsonify({"error": "Unable to retrieve logs"}), 500
```

## Explanation

The code constructs a file path using unsanitized user input from request parameters (device_id and log_type) directly in an f-string without validation. An attacker can inject path traversal sequences (../) to escape the intended directory and read arbitrary files on the system.

## Remediation

The fix applies a defense-in-depth approach: first, it validates that both device_id and log_type contain only safe alphanumeric characters (plus hyphens and underscores), rejecting any path traversal characters. Second, it resolves the constructed path to its canonical absolute form using os.path.realpath() and verifies the resolved path is still within the intended base directory, preventing any bypass through symlinks or encoded traversal sequences.

## Secure Code
```python
@app.route('/api/v2/iot/device/logs', methods=['GET'])
def fetch_device_logs():
    device_id = request.args.get('device_id')
    log_type = request.args.get('log_type', 'system')

    # Validate device_id and log_type contain only safe characters (alphanumeric, hyphens, underscores)
    if not device_id or not re.match(r'^[a-zA-Z0-9_\-]+$', device_id):
        return jsonify({"error": "Invalid device_id parameter"}), 400
    if not re.match(r'^[a-zA-Z0-9_\-]+$', log_type):
        return jsonify({"error": "Invalid log_type parameter"}), 400

    base_dir = '/var/iot/devices'
    log_path = os.path.join(base_dir, device_id, 'logs', f"{log_type}.log")

    # Resolve the absolute path and ensure it stays within the base directory
    resolved_path = os.path.realpath(log_path)
    if not resolved_path.startswith(os.path.realpath(base_dir) + os.sep):
        return jsonify({"error": "Access denied"}), 403

    try:
        with open(resolved_path, 'r') as log_file:
            log_content = log_file.read()
        return jsonify({"device": device_id, "logs": log_content, "status": "success"}), 200
    except FileNotFoundError:
        return jsonify({"error": "Log file not found"}), 404
    except Exception as e:
        return jsonify({"error": "Unable to retrieve logs"}), 500
```
