# Path Traversal via Unsafe os.path.join File Access

Language: Python
Severity: High
CWE: CWE-22

## Source
9

## Flow
9-10-11

## Sink
11

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

app = Flask(__name__)
CLOUD_BACKUP_DIR = '/var/backups/aws_snapshots'

@app.route('/api/v2/restore-snapshot', methods=['GET'])
def retrieve_aws_snapshot():
    snapshot_id = request.args.get('snapshot_name', 'default.tar.gz')
    backup_path = os.path.join(CLOUD_BACKUP_DIR, snapshot_id)
    if os.path.exists(backup_path):
        return send_file(backup_path, as_attachment=True)
    return {'error': 'Snapshot not found'}, 404
```

## Explanation

The application retrieves user-controlled input from the 'snapshot_name' query parameter without validation and directly passes it to os.path.join(). An attacker can use path traversal sequences (e.g., '../') to escape the intended CLOUD_BACKUP_DIR directory and access arbitrary files on the filesystem, which are then served via send_file().

## Remediation

The fix validates the user-supplied snapshot_name by first rejecting any input containing path separators or '..' sequences, then resolves the constructed path to its canonical absolute form using os.path.realpath() and verifies it remains within the intended CLOUD_BACKUP_DIR directory. This dual-layer approach prevents path traversal attacks even if an attacker uses encoded or alternative traversal techniques.

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

app = Flask(__name__)
CLOUD_BACKUP_DIR = '/var/backups/aws_snapshots'

@app.route('/api/v2/restore-snapshot', methods=['GET'])
def retrieve_aws_snapshot():
    snapshot_id = request.args.get('snapshot_name', 'default.tar.gz')
    # Reject any path separators or traversal sequences in the input
    if os.path.sep in snapshot_id or '/' in snapshot_id or '\\' in snapshot_id or '..' in snapshot_id:
        abort(400, description='Invalid snapshot name')
    backup_path = os.path.join(CLOUD_BACKUP_DIR, snapshot_id)
    # Resolve to absolute path and verify it stays within the allowed directory
    backup_path = os.path.realpath(backup_path)
    if not backup_path.startswith(os.path.realpath(CLOUD_BACKUP_DIR) + os.sep):
        abort(403, description='Access denied')
    if os.path.exists(backup_path):
        return send_file(backup_path, as_attachment=True)
    return {'error': 'Snapshot not found'}, 404
```
