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 os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tarfile
|
|
||||||
import textwrap
|
import textwrap
|
||||||
import traceback
|
import traceback
|
||||||
import typing
|
import typing
|
||||||
@@ -16,6 +15,7 @@ from typing import Any, Literal, TextIO
|
|||||||
|
|
||||||
import cibuildwheel
|
import cibuildwheel
|
||||||
from cibuildwheel import errors
|
from cibuildwheel import errors
|
||||||
|
from cibuildwheel._compat.tarfile import TarFile, safe_extractall
|
||||||
from cibuildwheel.architecture import Architecture, allowed_architectures_check
|
from cibuildwheel.architecture import Architecture, allowed_architectures_check
|
||||||
from cibuildwheel.ci import CIProvider, detect_ci_provider, fix_ansi_codes_for_github_actions
|
from cibuildwheel.ci import CIProvider, detect_ci_provider, fix_ansi_codes_for_github_actions
|
||||||
from cibuildwheel.logger import log
|
from cibuildwheel.logger import log
|
||||||
@@ -262,8 +262,8 @@ def main_inner(global_options: GlobalOptions) -> None:
|
|||||||
# Tarfile builds require extraction and changing the directory
|
# Tarfile builds require extraction and changing the directory
|
||||||
temp_dir = Path(mkdtemp(prefix="cibw-sdist-")).resolve(strict=True)
|
temp_dir = Path(mkdtemp(prefix="cibw-sdist-")).resolve(strict=True)
|
||||||
try:
|
try:
|
||||||
with tarfile.open(args.package_dir) as tar:
|
with TarFile.open(args.package_dir) as tar:
|
||||||
tar.extractall(path=temp_dir)
|
safe_extractall(tar, temp_dir)
|
||||||
|
|
||||||
# The extract directory is now the project dir
|
# The extract directory is now the project dir
|
||||||
try:
|
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