# Path Traversal via os.path.join() on Untrusted Filenames

Language: Python
Severity: Critical
CWE: CWE-22

## Source
6, 7

## Flow
6-9, 7-9

## Sink
9

## Vulnerable Code
```python
import os
from flask import request, send_file

@app.route('/api/v1/iot/device/logs')
def fetch_device_logs():
    device_id = request.args.get('device_id')
    log_filename = request.args.get('log_file', 'system.log')
    base_dir = '/var/iot/devices'
    log_path = os.path.join(base_dir, device_id, 'logs', log_filename)
    if os.path.exists(log_path):
        return send_file(log_path, as_attachment=True)
    return {'error': 'Log file not found'}, 404
```

## Explanation

The application accepts untrusted user input from 'device_id' and 'log_file' query parameters and directly passes them to os.path.join() without validation or sanitization. An attacker can inject path traversal sequences (../) in either parameter to escape the intended base directory and access arbitrary files on the filesystem, which are then served via send_file().

## Remediation

The fix applies a defense-in-depth approach: first, it validates both user inputs (device_id and log_filename) against strict allowlist regex patterns that reject path separators and traversal characters. Second, it resolves the constructed path to its canonical absolute form using os.path.realpath() and verifies the resolved path starts with the expected base directory prefix, preventing any bypass via symlinks or encoded traversal sequences.

## Secure Code
```python
import os
import re
from flask import request, send_file

@app.route('/api/v1/iot/device/logs')
def fetch_device_logs():
    device_id = request.args.get('device_id')
    log_filename = request.args.get('log_file', 'system.log')
    base_dir = '/var/iot/devices'

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

    # Validate log_filename: only allow alphanumeric characters, hyphens, underscores, and dots
    # Also reject any path separators or traversal sequences
    if not re.match(r'^[a-zA-Z0-9_\-\.]+$', log_filename):
        return {'error': 'Invalid log filename'}, 400

    # Construct the path
    log_path = os.path.join(base_dir, device_id, 'logs', log_filename)

    # Resolve to absolute path and verify it stays within the intended base directory
    resolved_path = os.path.realpath(log_path)
    expected_prefix = os.path.realpath(base_dir) + os.sep

    if not resolved_path.startswith(expected_prefix):
        return {'error': 'Access denied'}, 403

    if os.path.exists(resolved_path) and os.path.isfile(resolved_path):
        return send_file(resolved_path, as_attachment=True)
    return {'error': 'Log file not found'}, 404
```
