# Path Traversal

Language: Python
Severity: Critical
CWE: CWE-22

## Source
9

## Flow
9-11

## Sink
11

## Vulnerable Code
```python
from flask import Flask, request, send_file
import os
app = Flask(__name__)
SATELLITE_DATA_DIR = '/opt/telemetry/satellite_feeds'
@app.route('/api/iot/fetch_log')
def fetch_device_log():
    dev_id = request.args.get('device_id')
    log_name = request.args.get('log_file')
    resolved_path = os.path.join(SATELLITE_DATA_DIR, dev_id, log_name)
    if not os.path.exists(resolved_path):
        return {'error': 'log not found'}, 404
    return send_file(resolved_path)
```

## Explanation

Both `dev_id` (line 7) and `log_name` (line 8) are taken directly from user-supplied query parameters without any sanitization or validation. These two untrusted inputs are chained together via `os.path.join` (line 9) to construct a filesystem path, which is then passed to `send_file` (line 11). Because `os.path.join` does not normalize or restrict path traversal sequences, an attacker can supply `../` sequences in either parameter to escape the intended `SATELLITE_DATA_DIR` base directory and read arbitrary files on the server.

## Remediation

The fix applies a defense-in-depth strategy: first, both user-supplied parameters (`device_id` and `log_file`) are validated to reject path separators, traversal sequences (`..`), and null bytes. Second, after constructing the joined path, `os.path.realpath()` is used to resolve symlinks and normalize the path, and then a strict prefix check ensures the resolved path remains within the `SATELLITE_DATA_DIR` base directory. Additionally, `os.path.isfile()` replaces `os.path.exists()` to ensure only regular files (not directories) are served.

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

app = Flask(__name__)
SATELLITE_DATA_DIR = '/opt/telemetry/satellite_feeds'


def is_safe_path_component(component):
    """Validate that a path component does not contain traversal sequences or absolute paths."""
    if not component:
        return False
    # Reject any component containing path separators or traversal sequences
    if os.sep in component or '/' in component or '\\' in component:
        return False
    if component in ('.', '..'):
        return False
    if '\x00' in component:
        return False
    return True


@app.route('/api/iot/fetch_log')
def fetch_device_log():
    dev_id = request.args.get('device_id')
    log_name = request.args.get('log_file')

    # Reject missing parameters
    if not dev_id or not log_name:
        return {'error': 'device_id and log_file are required'}, 400

    # Validate individual path components to block traversal characters
    if not is_safe_path_component(dev_id) or not is_safe_path_component(log_name):
        return {'error': 'invalid device_id or log_file parameter'}, 400

    # Build the candidate path and resolve to an absolute, symlink-free path
    resolved_path = os.path.realpath(
        os.path.join(SATELLITE_DATA_DIR, dev_id, log_name)
    )

    # Ensure the resolved path is strictly within the allowed base directory
    base_dir = os.path.realpath(SATELLITE_DATA_DIR) + os.sep
    if not resolved_path.startswith(base_dir):
        return {'error': 'access denied'}, 403

    if not os.path.isfile(resolved_path):
        return {'error': 'log not found'}, 404

    return send_file(resolved_path)
```
