# Zip Slip via zipfile.extractall() on Untrusted Archives

Language: Python
Severity: Critical
CWE: CWE-22

## Source
7

## Flow
7-10-11

## Sink
11

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

@app.route('/iot/firmware/deploy', methods=['POST'])
def deploy_firmware_package():
    firmware_zip = 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)
    zip_path = f'/tmp/{firmware_zip.filename}'
    firmware_zip.save(zip_path)
    with zipfile.ZipFile(zip_path, 'r') as zf:
        zf.extractall(deployment_path)
    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})
```

## Explanation

The application accepts an uploaded ZIP file from an untrusted user and extracts it directly using zipfile.extractall() without validating the paths contained within the archive. A malicious actor can craft a ZIP file with entries containing directory traversal sequences (e.g., '../../../etc/cron.d/malicious') to write files outside the intended deployment_path, leading to arbitrary file write and potential remote code execution.

## Remediation

The fix validates each entry in the ZIP archive before extraction by resolving its absolute path and ensuring it remains within the intended deployment directory. Additionally, the device_id is validated against a whitelist pattern to prevent path traversal in the deployment path itself. If any archive entry would escape the target directory, the request is rejected with an error.

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

@app.route('/iot/firmware/deploy', methods=['POST'])
def deploy_firmware_package():
    firmware_zip = request.files['firmware_bundle']
    device_id = request.form.get('device_id')
    
    # Validate device_id to prevent path traversal in the deployment path itself
    if not device_id or not re.match(r'^[a-zA-Z0-9_\-]+$', 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)
    
    # Resolve the deployment path to its absolute canonical form
    abs_deployment_path = os.path.realpath(deployment_path)
    
    zip_path = f'/tmp/{firmware_zip.filename}'
    firmware_zip.save(zip_path)
    
    with zipfile.ZipFile(zip_path, 'r') as zf:
        for member in zf.namelist():
            # Compute the absolute path of the extracted file
            member_path = os.path.realpath(os.path.join(abs_deployment_path, member))
            
            # Ensure the resolved path is within the intended deployment directory
            if not member_path.startswith(abs_deployment_path + os.sep) and member_path != abs_deployment_path:
                os.remove(zip_path)
                return jsonify({
                    'status': 'error',
                    'message': f'Malicious path detected in archive: {member}'
                }), 400
        
        # All paths validated, safe to extract
        zf.extractall(abs_deployment_path)
    
    # Clean up the temporary zip file
    os.remove(zip_path)
    
    return jsonify({'status': 'deployed', 'device': device_id, 'path': deployment_path})
```
