# Tar Slip via Unsafe tarfile Extraction Path Traversal

Language: Python
Severity: Critical
CWE: CWE-22

## Source
8

## Flow
8-9

## Sink
9

## Vulnerable Code
```python
import tarfile
import os

def restore_iot_firmware_backup(backup_archive, device_id):
    restore_path = f"/opt/iot_devices/{device_id}/firmware"
    os.makedirs(restore_path, exist_ok=True)
    with tarfile.open(backup_archive, 'r:gz') as tar_handle:
        for archive_member in tar_handle.getmembers():
            tar_handle.extract(archive_member, path=restore_path)
    return {"status": "restored", "device": device_id, "location": restore_path}
```

## Explanation

The code extracts tar archive members without validating their paths, allowing path traversal attacks. An attacker can craft a malicious tar.gz file containing members with names like '../../../etc/cron.d/malicious' to write files outside the intended restore_path directory, potentially overwriting critical system files.

## Remediation

The fix validates that each tar archive member's resolved extraction path stays within the intended restore directory by using os.path.realpath to resolve the full path and checking it starts with the restore_path prefix. It also checks symbolic and hard links to ensure their targets don't point outside the allowed directory, preventing attackers from using crafted archives to write files to arbitrary locations on the filesystem.

## Secure Code
```python
import tarfile
import os

def restore_iot_firmware_backup(backup_archive, device_id):
    restore_path = os.path.realpath(f"/opt/iot_devices/{device_id}/firmware")
    os.makedirs(restore_path, exist_ok=True)
    with tarfile.open(backup_archive, 'r:gz') as tar_handle:
        for archive_member in tar_handle.getmembers():
            member_path = os.path.realpath(os.path.join(restore_path, archive_member.name))
            if not member_path.startswith(restore_path + os.sep) and member_path != restore_path:
                raise ValueError(f"Attempted path traversal in tar member: {archive_member.name}")
            if archive_member.issym() or archive_member.islnk():
                link_target = os.path.realpath(os.path.join(restore_path, archive_member.linkname))
                if not link_target.startswith(restore_path + os.sep) and link_target != restore_path:
                    raise ValueError(f"Attempted path traversal via symlink in tar member: {archive_member.name}")
            tar_handle.extract(archive_member, path=restore_path)
    return {"status": "restored", "device": device_id, "location": restore_path}
```
