fix: tarfile extraction filter (#2856)
* fix: tarfile extraction filter From pypa/build, with pathlib improvements and adapted for 3.11+. See #2854. Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * Update cibuildwheel/_compat/tarfile.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Add source reference comment to tarfile.py Add header comment indicating source of the file. --------- Signed-off-by: Henry Schreiner <henryfs@princeton.edu> Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com>
This commit is contained in:
co-authored by
Agriya Khetarpal
parent
36b8e7e524
commit
d08d9acda2
@@ -5,7 +5,6 @@ import functools
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tarfile
|
||||
import textwrap
|
||||
import traceback
|
||||
import typing
|
||||
@@ -16,6 +15,7 @@ from typing import Any, Literal, TextIO
|
||||
|
||||
import cibuildwheel
|
||||
from cibuildwheel import errors
|
||||
from cibuildwheel._compat.tarfile import TarFile, safe_extractall
|
||||
from cibuildwheel.architecture import Architecture, allowed_architectures_check
|
||||
from cibuildwheel.ci import CIProvider, detect_ci_provider, fix_ansi_codes_for_github_actions
|
||||
from cibuildwheel.logger import log
|
||||
@@ -262,8 +262,8 @@ def main_inner(global_options: GlobalOptions) -> None:
|
||||
# Tarfile builds require extraction and changing the directory
|
||||
temp_dir = Path(mkdtemp(prefix="cibw-sdist-")).resolve(strict=True)
|
||||
try:
|
||||
with tarfile.open(args.package_dir) as tar:
|
||||
tar.extractall(path=temp_dir)
|
||||
with TarFile.open(args.package_dir) as tar:
|
||||
safe_extractall(tar, temp_dir)
|
||||
|
||||
# The extract directory is now the project dir
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Based on https://github.com/pypa/build/blob/f4ebd495cc0c2c74155bd4fe48b76399fb7927ac/src/build/_compat/tarfile.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tarfile
|
||||
|
||||
TYPE_CHECKING = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
TarFile = tarfile.TarFile
|
||||
|
||||
# Per https://peps.python.org/pep-0706/, the "data" filter will become
|
||||
# the default in Python 3.14. The first series of releases with the filter
|
||||
# had a broken filter that could not process symlinks correctly.
|
||||
elif (3, 11, 5) <= sys.version_info < (3, 14):
|
||||
|
||||
class TarFile(tarfile.TarFile): # pragma: no cover
|
||||
extraction_filter = staticmethod(tarfile.data_filter)
|
||||
|
||||
else:
|
||||
TarFile = tarfile.TarFile # pragma: no cover
|
||||
|
||||
|
||||
# Same availability matrix as the TarFile subclass above. On runtimes that
|
||||
# ship the stdlib ``data`` filter we delegate to it; the fallback branch is
|
||||
# only reached on 3.10.0-3.10.12 / 3.11.0-3.11.4 and validates each member
|
||||
# manually before extraction.
|
||||
if sys.version_info >= (3, 11, 5):
|
||||
|
||||
def safe_extractall(tar: tarfile.TarFile, path: Path) -> None: # pragma: no cover
|
||||
"""Extract every member of ``tar`` into ``path`` via the PEP 706 ``data`` filter."""
|
||||
tar.extractall(path, filter="data")
|
||||
|
||||
else:
|
||||
|
||||
def safe_extractall(tar: tarfile.TarFile, path: Path) -> None: # pragma: no cover
|
||||
"""Validate every member of ``tar``, then extract into ``path``.
|
||||
|
||||
Reached on 3.10.0-3.10.12 / 3.11.0-3.11.4 where the stdlib ``data`` filter is missing. Device or special files,
|
||||
paths that escape ``path``, and symlinks/hardlinks whose targets resolve outside ``path`` are rejected before
|
||||
any write hits the disk.
|
||||
|
||||
"""
|
||||
base = path.resolve()
|
||||
for member in tar.getmembers():
|
||||
_validate_safe_member(member, base)
|
||||
tar.extractall(path)
|
||||
|
||||
|
||||
def _validate_safe_member(member: tarfile.TarInfo, base: Path) -> None:
|
||||
if member.ischr() or member.isblk() or member.isfifo():
|
||||
msg = f"refusing to extract special device file {member.name!r}"
|
||||
raise tarfile.TarError(msg)
|
||||
target = (base / member.name).resolve(strict=False)
|
||||
if not target.is_relative_to(base):
|
||||
msg = f"refusing to extract {member.name!r}: path escapes destination"
|
||||
raise tarfile.TarError(msg)
|
||||
if member.issym() or member.islnk():
|
||||
link_base = target.parent if member.issym() else base
|
||||
link_target = (link_base / member.linkname).resolve(strict=False)
|
||||
if not link_target.is_relative_to(base):
|
||||
msg = f"refusing to extract {member.name!r}: link target escapes destination"
|
||||
raise tarfile.TarError(msg)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"TarFile",
|
||||
"safe_extractall",
|
||||
]
|
||||
Reference in New Issue
Block a user