# Path Traversal via os.path.join on Untrusted Filename

Language: Python
Severity: High
CWE: CWE-22

## Source
8,9

## Flow
8-9-11-12

## Sink
12

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

app = Flask(__name__)

@app.route('/api/iot/firmware/download', methods=['GET'])
def retrieve_firmware_binary():
    device_model = request.args.get('model', 'default')
    fw_version = request.args.get('version', 'latest')
    firmware_base = '/var/iot/firmware_repository'
    fw_filename = f"{device_model}_{fw_version}.bin"
    full_fw_path = os.path.join(firmware_base, fw_filename)
    if os.path.exists(full_fw_path):
        return send_file(full_fw_path, as_attachment=True)
    return {'error': 'Firmware not found'}, 404
```

## Explanation

User-controlled parameters 'model' and 'version' are concatenated into a filename without validation, allowing path traversal sequences like '../'. When passed to os.path.join() and subsequently send_file(), an attacker can traverse directories to access arbitrary files on the filesystem.

## Remediation

The fix applies two layers of defense: first, it validates that both user inputs (model and version) contain only safe alphanumeric characters, hyphens, underscores, and dots using a regex whitelist, rejecting any path traversal characters. Second, it resolves the constructed path to its canonical absolute form using os.path.realpath() and verifies it remains within the intended firmware base directory, preventing any bypass through symlinks or encoded traversal sequences.

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

app = Flask(__name__)

@app.route('/api/iot/firmware/download', methods=['GET'])
def retrieve_firmware_binary():
    device_model = request.args.get('model', 'default')
    fw_version = request.args.get('version', 'latest')
    firmware_base = '/var/iot/firmware_repository'

    # Validate that model and version contain only safe characters (alphanumeric, hyphens, underscores, dots)
    safe_pattern = re.compile(r'^[a-zA-Z0-9._-]+$')
    if not safe_pattern.match(device_model) or not safe_pattern.match(fw_version):
        return {'error': 'Invalid model or version parameter'}, 400

    fw_filename = f"{device_model}_{fw_version}.bin"
    full_fw_path = os.path.join(firmware_base, fw_filename)

    # Resolve to absolute path and verify it stays within the firmware base directory
    resolved_path = os.path.realpath(full_fw_path)
    if not resolved_path.startswith(os.path.realpath(firmware_base) + os.sep):
        return {'error': 'Invalid firmware path'}, 400

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