# Zip Slip via zipfile.extractall()

Language: Python
Severity: Critical
CWE: CWE-22

## Source
8

## Flow
8-10-11-13

## Sink
13

## Vulnerable Code
```python
import zipfile
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 = os.path.join('/tmp', firmware_archive.filename)
    firmware_archive.save(archive_path)
    with zipfile.ZipFile(archive_path, 'r') as fw_zip:
        fw_zip.extractall(deployment_path)
    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})
```

## Explanation

The code extracts a ZIP archive using zipfile.extractall() without validating the file paths within the archive. A malicious ZIP file can contain entries with path traversal sequences (e.g., ../../etc/passwd) that allow writing files outside the intended deployment_path directory, leading to arbitrary file write vulnerabilities (Zip Slip).

## Remediation

The fix validates each file path within the ZIP archive before extraction by resolving the full real path and ensuring it stays within the intended deployment directory. Additionally, the device_id is validated against a whitelist pattern to prevent path traversal via the directory name itself, and the temporary archive file is cleaned up after use.

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

@app.route('/api/iot/firmware/deploy', methods=['POST'])
def deploy_firmware_package():
    device_id = request.form.get('device_id')
    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)
    archive_path = os.path.join('/tmp', firmware_archive.filename)
    firmware_archive.save(archive_path)
    with zipfile.ZipFile(archive_path, 'r') as fw_zip:
        for member in fw_zip.namelist():
            member_path = os.path.realpath(os.path.join(deployment_path, member))
            if not member_path.startswith(os.path.realpath(deployment_path) + os.sep) and member_path != os.path.realpath(deployment_path):
                os.remove(archive_path)
                return jsonify({'status': 'error', 'message': f'Illegal path in archive: {member}'}), 400
        fw_zip.extractall(deployment_path)
    os.remove(archive_path)
    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})
```
