# Directory Traversal via Unvalidated File Path Join

Language: Python
Severity: Critical
CWE: CWE-22

## Source
10-12

## Flow
10-13-14

## Sink
14

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

app = Flask(__name__)
LOG_STORAGE = '/var/iot/device_logs/'

@app.route('/api/v2/device/fetch_diagnostic', methods=['GET'])
def fetch_diagnostic():
    device_id = request.args.get('device_id', 'unknown')
    log_type = request.args.get('log_type', 'system')
    log_filename = request.args.get('filename', 'latest.log')
    device_log_path = os.path.join(LOG_STORAGE, device_id, log_type, log_filename)
    if os.path.exists(device_log_path):
        return send_file(device_log_path, as_attachment=True)
    return {'error': 'Diagnostic log not found'}, 404
```

## Explanation

The application constructs file paths using unsanitized user input from query parameters (device_id, log_type, filename) directly in os.path.join(). An attacker can inject path traversal sequences (../) in any of these parameters to access arbitrary files outside the intended LOG_STORAGE directory, bypassing the directory structure entirely.

## Remediation

The fix applies a defense-in-depth approach: first, each user-supplied path component is validated against an allowlist regex that permits only alphanumeric characters, hyphens, underscores, and dots (but not leading dots), rejecting any traversal sequences or special characters. Second, after constructing the path, os.path.realpath() resolves any symbolic links or remaining traversal attempts, and the resolved path is verified to start with the intended LOG_STORAGE base directory. Additionally, os.path.isfile() ensures only regular files are served.

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

app = Flask(__name__)
LOG_STORAGE = '/var/iot/device_logs/'

def is_safe_path_component(component):
    """Validate that a path component contains only safe characters and no traversal sequences."""
    if not component:
        return False
    if '..' in component or '/' in component or '\\' in component:
        return False
    if not re.match(r'^[a-zA-Z0-9_\-\.]+$', component):
        return False
    if component.startswith('.'):
        return False
    return True

@app.route('/api/v2/device/fetch_diagnostic', methods=['GET'])
def fetch_diagnostic():
    device_id = request.args.get('device_id', 'unknown')
    log_type = request.args.get('log_type', 'system')
    log_filename = request.args.get('filename', 'latest.log')

    # Validate each path component individually
    if not is_safe_path_component(device_id):
        return {'error': 'Invalid device_id'}, 400
    if not is_safe_path_component(log_type):
        return {'error': 'Invalid log_type'}, 400
    if not is_safe_path_component(log_filename):
        return {'error': 'Invalid filename'}, 400

    device_log_path = os.path.join(LOG_STORAGE, device_id, log_type, log_filename)

    # Resolve to absolute path and verify it stays within LOG_STORAGE
    resolved_path = os.path.realpath(device_log_path)
    resolved_base = os.path.realpath(LOG_STORAGE)

    if not resolved_path.startswith(resolved_base + os.sep):
        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': 'Diagnostic log not found'}, 404
```
