# Tar Path Traversal via tarfile.extractall() Member Extraction

Language: Python
Severity: Critical
CWE: CWE-22

## Source
10

## Flow
10-17

## Sink
17

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

app = Flask(__name__)

@app.route('/api/iot/firmware/deploy', methods=['POST'])
def deploy_firmware_package():
    firmware_archive = request.files['firmware_bundle']
    device_id = request.form.get('device_id')
    deployment_path = f'/opt/iot_devices/{device_id}/firmware'
    os.makedirs(deployment_path, exist_ok=True)
    archive_location = f'/tmp/{firmware_archive.filename}'
    firmware_archive.save(archive_location)
    with tarfile.open(archive_location, 'r:gz') as tar_bundle:
        tar_bundle.extractall(path=deployment_path)
    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})
```

## Explanation

The code accepts a tar archive via user upload and extracts it using tarfile.extractall() without validating member paths. A malicious archive can contain path traversal sequences (e.g., ../../etc/cron.d/malicious) allowing attackers to write files outside the intended deployment directory and potentially achieve code execution or system compromise.

## Remediation

The fix validates every tar archive member's resolved path to ensure it stays within the intended deployment directory before extraction, rejecting archives containing path traversal sequences or malicious symlinks. Additionally, the device_id input is sanitized to prevent directory traversal in the deployment path itself, and the archived filename is sanitized using os.path.basename().

## Secure Code
```python
import tarfile
import os
from flask import Flask, request, jsonify

app = Flask(__name__)

def is_safe_tar_member(member, destination):
    """Validate that a tar member extracts safely within the destination directory."""
    member_path = os.path.realpath(os.path.join(destination, member.name))
    destination_path = os.path.realpath(destination)
    if not member_path.startswith(destination_path + os.sep) and member_path != destination_path:
        return False
    if member.issym() or member.islnk():
        link_target = os.path.realpath(os.path.join(destination, member.linkname))
        if not link_target.startswith(destination_path + os.sep):
            return False
    return True

@app.route('/api/iot/firmware/deploy', methods=['POST'])
def deploy_firmware_package():
    firmware_archive = request.files['firmware_bundle']
    device_id = request.form.get('device_id')
    if not device_id or '..' in device_id or '/' in device_id or '\\' in device_id:
        return jsonify({'status': 'error', 'message': 'Invalid device_id'}), 400
    deployment_path = f'/opt/iot_devices/{device_id}/firmware'
    os.makedirs(deployment_path, exist_ok=True)
    archive_location = f'/tmp/{os.path.basename(firmware_archive.filename)}'
    firmware_archive.save(archive_location)
    try:
        with tarfile.open(archive_location, 'r:gz') as tar_bundle:
            for member in tar_bundle.getmembers():
                if not is_safe_tar_member(member, deployment_path):
                    os.remove(archive_location)
                    return jsonify({'status': 'error', 'message': f'Unsafe path detected in archive: {member.name}'}), 400
            tar_bundle.extractall(path=deployment_path, members=tar_bundle.getmembers())
    finally:
        if os.path.exists(archive_location):
            os.remove(archive_location)
    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})
```
