# Zip Slip via zipfile.extractall()

Language: Python
Severity: Critical
CWE: CWE-22

## Source
8

## Flow
8-13-17

## Sink
17

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

app = Flask(__name__)

@app.route('/api/v1/firmware/upload', methods=['POST'])
def process_iot_firmware_package():
    if 'firmware_bundle' not in request.files:
        return jsonify({'error': 'No firmware file provided'}), 400
    
    fw_archive = request.files['firmware_bundle']
    device_id = request.form.get('device_id', 'unknown')
    staging_path = f'/var/iot/firmware_staging/{device_id}'
    
    os.makedirs(staging_path, exist_ok=True)
    archive_path = os.path.join(staging_path, fw_archive.filename)
    fw_archive.save(archive_path)
    
    with zipfile.ZipFile(archive_path, 'r') as zip_ref:
        zip_ref.extractall(staging_path)
    
    manifest_file = os.path.join(staging_path, 'manifest.json')
    if os.path.exists(manifest_file):
        return jsonify({'status': 'Firmware extracted successfully', 'device': device_id}), 200
    return jsonify({'error': 'Invalid firmware package'}), 400
```

## Explanation

The application accepts an uploaded ZIP file and extracts it using zipfile.extractall() without validating that archive entries stay within the intended directory. A malicious ZIP file can contain entries with path traversal sequences (e.g., '../../../../etc/cron.d/malicious') that write files outside the staging directory, leading to arbitrary file write and potential remote code execution.

## Remediation

The fix introduces a safe_extract() function that validates each ZIP entry's resolved path stays within the intended destination directory before extraction, preventing Zip Slip attacks. Additionally, the device_id and uploaded filename are sanitized using os.path.basename() to prevent path traversal through form inputs, and proper error handling is added for malicious or invalid archives.

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

app = Flask(__name__)


def safe_extract(zip_ref, destination):
    """Safely extract ZIP contents, preventing path traversal (Zip Slip)."""
    destination = os.path.realpath(destination)
    for member in zip_ref.namelist():
        member_path = os.path.realpath(os.path.join(destination, member))
        if not member_path.startswith(destination + os.sep) and member_path != destination:
            raise ValueError(f"Attempted path traversal in zip entry: {member}")
    zip_ref.extractall(destination)


@app.route('/api/v1/firmware/upload', methods=['POST'])
def process_iot_firmware_package():
    if 'firmware_bundle' not in request.files:
        return jsonify({'error': 'No firmware file provided'}), 400
    
    fw_archive = request.files['firmware_bundle']
    device_id = request.form.get('device_id', 'unknown')
    
    # Sanitize device_id to prevent path traversal via form input
    device_id = os.path.basename(device_id)
    if not device_id or device_id in ('.', '..'):
        return jsonify({'error': 'Invalid device ID'}), 400
    
    staging_path = f'/var/iot/firmware_staging/{device_id}'
    
    os.makedirs(staging_path, exist_ok=True)
    
    # Sanitize the uploaded filename
    safe_filename = os.path.basename(fw_archive.filename)
    if not safe_filename:
        return jsonify({'error': 'Invalid filename'}), 400
    
    archive_path = os.path.join(staging_path, safe_filename)
    fw_archive.save(archive_path)
    
    try:
        with zipfile.ZipFile(archive_path, 'r') as zip_ref:
            safe_extract(zip_ref, staging_path)
    except ValueError as e:
        os.remove(archive_path)
        return jsonify({'error': f'Malicious archive detected: {str(e)}'}), 400
    except zipfile.BadZipFile:
        os.remove(archive_path)
        return jsonify({'error': 'Invalid ZIP file'}), 400
    
    manifest_file = os.path.join(staging_path, 'manifest.json')
    if os.path.exists(manifest_file):
        return jsonify({'status': 'Firmware extracted successfully', 'device': device_id}), 200
    return jsonify({'error': 'Invalid firmware package'}), 400
```
