# Path Traversal via zipfile.extractall() Zip Slip

Language: Python
Severity: Critical
CWE: CWE-22

## Source
8

## Flow
8-9-10-12-13-15-17

## Sink
17

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

app = Flask(__name__)

@app.route('/api/v1/deploy-model', methods=['POST'])
def deploy_ml_model():
    if 'model_package' not in request.files:
        return jsonify({'error': 'No model package provided'}), 400
    model_zip = request.files['model_package']
    model_id = request.form.get('model_id', 'default_model')
    deployment_path = f'/opt/ml_models/{model_id}'
    os.makedirs(deployment_path, exist_ok=True)
    zip_path = os.path.join('/tmp', f'{model_id}.zip')
    model_zip.save(zip_path)
    with zipfile.ZipFile(zip_path, 'r') as archive:
        archive.extractall(deployment_path)
    os.remove(zip_path)
    return jsonify({'status': 'deployed', 'model_id': model_id, 'path': deployment_path}), 200
```

## Explanation

The application extracts a user-uploaded ZIP file without validating the paths of entries within the archive. An attacker can craft a malicious ZIP file containing entries with path traversal sequences (e.g., '../../../../etc/cron.d/malicious') that escape the intended deployment_path directory when extractall() is called, allowing arbitrary file writes on the server filesystem.

## Remediation

The fix validates each entry in the ZIP archive before extraction by resolving the full real path and ensuring it stays within the intended deployment directory. Additionally, the model_id input is sanitized with a whitelist regex to prevent path traversal in the directory creation step. If any malicious path is detected, the request is rejected with a 400 error.

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

app = Flask(__name__)

@app.route('/api/v1/deploy-model', methods=['POST'])
def deploy_ml_model():
    if 'model_package' not in request.files:
        return jsonify({'error': 'No model package provided'}), 400
    model_zip = request.files['model_package']
    model_id = request.form.get('model_id', 'default_model')
    # Sanitize model_id to prevent path traversal in directory creation
    if not re.match(r'^[a-zA-Z0-9_\-]+$', model_id):
        return jsonify({'error': 'Invalid model_id. Only alphanumeric characters, hyphens, and underscores are allowed.'}), 400
    deployment_path = f'/opt/ml_models/{model_id}'
    os.makedirs(deployment_path, exist_ok=True)
    zip_path = os.path.join('/tmp', f'{model_id}.zip')
    model_zip.save(zip_path)
    with zipfile.ZipFile(zip_path, 'r') as archive:
        # Validate all entries before extraction to prevent Zip Slip
        for member in archive.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(zip_path)
                return jsonify({'error': f'Malicious path detected in archive: {member}'}), 400
        archive.extractall(deployment_path)
    os.remove(zip_path)
    return jsonify({'status': 'deployed', 'model_id': model_id, 'path': deployment_path}), 200
```
