# Tar Slip via tarfile.extractall()

Language: Python
Severity: Critical
CWE: CWE-22

## Source
7

## Flow
7-11-12

## Sink
12

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

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

## Explanation

The application accepts a user-controlled filename from an uploaded file without validation and uses it to save the archive. Subsequently, tarfile.extractall() is called without validating archive member paths, allowing an attacker to craft a malicious tar archive with paths like '../../../etc/cron.d/malicious' to write files outside the intended deployment directory (path traversal/tar slip vulnerability).

## Remediation

The fix adds comprehensive path validation for all tar archive members before extraction, ensuring no member can escape the intended deployment directory via relative paths (../) or symlinks. Additionally, the device_id is validated against a whitelist pattern to prevent directory traversal in the deployment path, and the uploaded file is saved with a randomly generated filename to prevent filename-based attacks.

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

def _is_safe_path(base_path, target_path):
    """Verify that the target path is within the base directory."""
    abs_base = os.path.realpath(base_path)
    abs_target = os.path.realpath(target_path)
    return abs_target.startswith(abs_base + os.sep) or abs_target == abs_base

def _validate_tar_members(tar, destination):
    """Validate all tar members to prevent path traversal (tar slip)."""
    for member in tar.getmembers():
        member_path = os.path.join(destination, member.name)
        if not _is_safe_path(destination, member_path):
            raise ValueError(f"Attempted path traversal in tar member: {member.name}")
        if member.issym() or member.islnk():
            link_target = os.path.join(destination, os.path.dirname(member.name), member.linkname)
            if not _is_safe_path(destination, link_target):
                raise ValueError(f"Attempted path traversal via symlink in tar member: {member.name}")
    return tar.getmembers()

@app.route('/api/iot/firmware/deploy', methods=['POST'])
def deploy_firmware_package():
    device_id = request.form.get('device_id')

    # Validate device_id to prevent path traversal in directory creation
    if not device_id or not re.match(r'^[a-zA-Z0-9_\-]+$', device_id):
        return jsonify({'status': 'error', 'message': 'Invalid device_id'}), 400

    firmware_archive = request.files['firmware']
    deployment_path = f'/opt/iot/devices/{device_id}/firmware'
    os.makedirs(deployment_path, exist_ok=True)

    # Use a safe, unique filename instead of user-controlled filename
    safe_filename = f'{uuid.uuid4().hex}.tar.gz'
    archive_path = os.path.join('/tmp', safe_filename)
    firmware_archive.save(archive_path)

    try:
        with tarfile.open(archive_path, 'r:gz') as tar_pkg:
            # Validate all members before extraction
            safe_members = _validate_tar_members(tar_pkg, deployment_path)
            tar_pkg.extractall(path=deployment_path, members=safe_members)
    except (ValueError, tarfile.TarError) as e:
        os.remove(archive_path)
        return jsonify({'status': 'error', 'message': str(e)}), 400
    finally:
        if os.path.exists(archive_path):
            os.remove(archive_path)

    return jsonify({'status': 'deployed', 'device': device_id, 'location': deployment_path})
```
