{"title":"Tar Slip via tarfile.extractall() Path Traversal","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[6,7],"flow_lines":[6,7,8,11,12],"sink_lines":[12],"vulnerable_code":"import tarfile\nimport os\nfrom flask import request, jsonify\n\n@app.route('/iot/firmware/deploy', methods=['POST'])\ndef deploy_firmware_package():\n    device_id = request.form.get('device_id')\n    firmware_archive = request.files['firmware']\n    deployment_path = f'/opt/iot/devices/{device_id}/firmware'\n    os.makedirs(deployment_path, exist_ok=True)\n    archive_path = os.path.join(deployment_path, 'package.tar.gz')\n    firmware_archive.save(archive_path)\n    with tarfile.open(archive_path, 'r:gz') as tar_pkg:\n        tar_pkg.extractall(path=deployment_path)\n    return jsonify({'status': 'deployed', 'device': device_id, 'location': deployment_path})","explanation":"The application accepts an untrusted device_id parameter and uses it to construct the extraction path without validation. A malicious tar archive can contain files with path traversal sequences (../) in their names, allowing tarfile.extractall() to write files outside the intended directory, potentially overwriting critical system files or injecting malicious code.","remediation":"The fix addresses both vulnerability points: (1) the device_id parameter is validated with a strict regex whitelist to prevent path traversal in the constructed directory path, and (2) a safe extraction function validates every tar member's resolved path to ensure it stays within the intended deployment directory before extraction, including checks for symlink-based traversal attacks.","secure_code":"import tarfile\nimport os\nimport re\nfrom flask import request, jsonify\n\ndef _is_safe_path(base_path, target_path):\n    \"\"\"Ensure the target path is within the base directory.\"\"\"\n    abs_base = os.path.realpath(base_path)\n    abs_target = os.path.realpath(target_path)\n    return abs_target.startswith(abs_base + os.sep) or abs_target == abs_base\n\ndef _safe_extract(tar, destination):\n    \"\"\"Extract tar members only if they resolve within the destination directory.\"\"\"\n    for member in tar.getmembers():\n        member_path = os.path.join(destination, member.name)\n        abs_member_path = os.path.realpath(member_path)\n        if not abs_member_path.startswith(os.path.realpath(destination) + os.sep):\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(destination, os.path.dirname(member.name), member.linkname)\n            abs_link_target = os.path.realpath(link_target)\n            if not abs_link_target.startswith(os.path.realpath(destination) + os.sep):\n                raise ValueError(f\"Attempted path traversal via symlink in tar member: {member.name}\")\n    tar.extractall(path=destination)\n\n@app.route('/iot/firmware/deploy', methods=['POST'])\ndef deploy_firmware_package():\n    device_id = request.form.get('device_id')\n    if not device_id or not re.match(r'^[a-zA-Z0-9_\\-]+$', device_id):\n        return jsonify({'status': 'error', 'message': 'Invalid device_id'}), 400\n    firmware_archive = request.files['firmware']\n    deployment_path = f'/opt/iot/devices/{device_id}/firmware'\n    base_dir = '/opt/iot/devices'\n    if not _is_safe_path(base_dir, deployment_path):\n        return jsonify({'status': 'error', 'message': 'Invalid deployment path'}), 400\n    os.makedirs(deployment_path, exist_ok=True)\n    archive_path = os.path.join(deployment_path, 'package.tar.gz')\n    firmware_archive.save(archive_path)\n    try:\n        with tarfile.open(archive_path, 'r:gz') as tar_pkg:\n            _safe_extract(tar_pkg, deployment_path)\n    except (ValueError, tarfile.TarError) as e:\n        os.remove(archive_path)\n        return jsonify({'status': 'error', 'message': f'Invalid firmware package: {str(e)}'}), 400\n    return jsonify({'status': 'deployed', 'device': device_id, 'location': deployment_path})"}