{"title":"Tar Path Traversal via tarfile.extractall()","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[6],"flow_lines":[6,7,10,12],"sink_lines":[12],"vulnerable_code":"import tarfile\nimport os\nfrom flask import request, jsonify\n\n@app.route('/api/cloud/deploy-model', methods=['POST'])\ndef deploy_ml_model():\n    model_archive = request.files['model_package']\n    deployment_id = request.form.get('deployment_id')\n    extract_path = f'/opt/ml_models/{deployment_id}'\n    os.makedirs(extract_path, exist_ok=True)\n    archive_path = f'/tmp/{model_archive.filename}'\n    model_archive.save(archive_path)\n    with tarfile.open(archive_path, 'r:gz') as tar:\n        tar.extractall(path=extract_path)\n    return jsonify({'status': 'deployed', 'path': extract_path})","explanation":"The code extracts a user-uploaded tar archive using tarfile.extractall() without validating the paths of archive members. A malicious tar file can contain entries with absolute paths or path traversal sequences (e.g., '../../etc/crontab') that write files outside the intended extraction directory, leading to arbitrary file write vulnerabilities.","remediation":"The fix validates every tar archive member's resolved path to ensure it stays within the intended extraction directory before calling extractall(). It also checks symlink targets to prevent indirect path traversal, sanitizes the deployment_id parameter against directory traversal, uses os.path.basename on the uploaded filename, and cleans up temporary files after extraction.","secure_code":"import tarfile\nimport os\nfrom flask import request, jsonify\n\ndef _is_within_directory(directory, target):\n    abs_directory = os.path.realpath(directory)\n    abs_target = os.path.realpath(target)\n    return abs_target.startswith(abs_directory + os.sep) or abs_target == abs_directory\n\ndef _safe_extract(tar, extract_path):\n    for member in tar.getmembers():\n        member_path = os.path.join(extract_path, member.name)\n        if not _is_within_directory(extract_path, member_path):\n            raise ValueError(f\"Attempted path traversal in tar member: {member.name}\")\n        if member.issym() or member.islnk():\n            link_target = os.path.join(extract_path, os.path.normpath(member.linkname)) if not os.path.isabs(member.linkname) else member.linkname\n            if not _is_within_directory(extract_path, link_target):\n                raise ValueError(f\"Attempted path traversal via symlink in tar member: {member.name}\")\n    tar.extractall(path=extract_path)\n\n@app.route('/api/cloud/deploy-model', methods=['POST'])\ndef deploy_ml_model():\n    model_archive = request.files['model_package']\n    deployment_id = request.form.get('deployment_id')\n\n    if not deployment_id or '..' in deployment_id or '/' in deployment_id or '\\\\' in deployment_id:\n        return jsonify({'error': 'Invalid deployment_id'}), 400\n\n    extract_path = f'/opt/ml_models/{deployment_id}'\n    os.makedirs(extract_path, exist_ok=True)\n    archive_path = f'/tmp/{os.path.basename(model_archive.filename)}'\n    model_archive.save(archive_path)\n\n    try:\n        with tarfile.open(archive_path, 'r:gz') as tar:\n            _safe_extract(tar, extract_path)\n    except (ValueError, tarfile.TarError) as e:\n        os.remove(archive_path)\n        return jsonify({'error': str(e)}), 400\n    finally:\n        if os.path.exists(archive_path):\n            os.remove(archive_path)\n\n    return jsonify({'status': 'deployed', 'path': extract_path})"}