{"title":"Tar Slip via tarfile.extractall() Path Traversal","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[8],"flow_lines":[8,9,10,13,14,15],"sink_lines":[15],"vulnerable_code":"import tarfile\nimport os\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/api/v1/deploy-model', methods=['POST'])\ndef deploy_ml_model():\n    model_archive = request.files.get('model_package')\n    deployment_id = request.form.get('deployment_id', 'default')\n    extraction_path = f'/var/ml_models/{deployment_id}'\n    os.makedirs(extraction_path, exist_ok=True)\n    archive_path = f'/tmp/model_{deployment_id}.tar.gz'\n    model_archive.save(archive_path)\n    with tarfile.open(archive_path, 'r:gz') as tar_ref:\n        tar_ref.extractall(path=extraction_path)\n    os.remove(archive_path)\n    return jsonify({'status': 'deployed', 'model_path': extraction_path, 'deployment_id': deployment_id})","explanation":"The code accepts a tar.gz archive from untrusted user input and extracts it using tarfile.extractall() without validating the paths within the archive. A malicious archive can contain entries with path traversal sequences (../) that escape the intended extraction directory and overwrite arbitrary files on the system.","remediation":"The fix adds a safe_extract function that validates every tar archive member before extraction, ensuring no member's resolved path escapes the intended extraction directory. It checks both regular file paths and symbolic/hard link targets for path traversal attempts, and also sanitizes the deployment_id input to prevent directory traversal via the folder name itself.","secure_code":"import tarfile\nimport os\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n\ndef is_safe_tar_member(member, extraction_path):\n    \"\"\"Validate that a tar member extracts safely within the target directory.\"\"\"\n    member_path = os.path.realpath(os.path.join(extraction_path, member.name))\n    if not member_path.startswith(os.path.realpath(extraction_path) + os.sep) and member_path != os.path.realpath(extraction_path):\n        return False\n    if member.issym() or member.islnk():\n        link_target = os.path.realpath(os.path.join(extraction_path, member.linkname))\n        if not link_target.startswith(os.path.realpath(extraction_path) + os.sep):\n            return False\n    return True\n\n\ndef safe_extract(tar_ref, extraction_path):\n    \"\"\"Extract tar members only if they are safe (no path traversal).\"\"\"\n    for member in tar_ref.getmembers():\n        if not is_safe_tar_member(member, extraction_path):\n            raise ValueError(f\"Unsafe path detected in archive: {member.name}\")\n    tar_ref.extractall(path=extraction_path)\n\n\n@app.route('/api/v1/deploy-model', methods=['POST'])\ndef deploy_ml_model():\n    model_archive = request.files.get('model_package')\n    if not model_archive:\n        return jsonify({'status': 'error', 'message': 'No model package provided'}), 400\n\n    deployment_id = request.form.get('deployment_id', 'default')\n\n    if not deployment_id.isalnum() and not all(c in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-' for c in deployment_id):\n        return jsonify({'status': 'error', 'message': 'Invalid deployment_id'}), 400\n\n    extraction_path = f'/var/ml_models/{deployment_id}'\n    os.makedirs(extraction_path, exist_ok=True)\n    archive_path = f'/tmp/model_{deployment_id}.tar.gz'\n    model_archive.save(archive_path)\n\n    try:\n        with tarfile.open(archive_path, 'r:gz') as tar_ref:\n            safe_extract(tar_ref, extraction_path)\n    except (ValueError, tarfile.TarError) as e:\n        os.remove(archive_path)\n        return jsonify({'status': 'error', 'message': str(e)}), 400\n\n    os.remove(archive_path)\n    return jsonify({'status': 'deployed', 'model_path': extraction_path, 'deployment_id': deployment_id})"}