Skip to content

Commit 2490eb2

Browse files
author
user
committed
Check symlink target in tar extraction fallback for Pythons without data_filter
1 parent d52011f commit 2490eb2

File tree

2 files changed

+119
-0
lines changed

2 files changed

+119
-0
lines changed

src/pip/_internal/utils/unpacking.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,17 @@ def _untar_without_filter(
255255
leading: bool,
256256
) -> None:
257257
"""Fallback for Python without tarfile.data_filter"""
258+
259+
def _check_link_target(tar: tarfile.TarFile, tarinfo: tarfile.TarInfo) -> None:
260+
linkname = "/".join(
261+
filter(None, (os.path.dirname(tarinfo.name), tarinfo.linkname))
262+
)
263+
264+
try:
265+
tar.getmember(linkname)
266+
except KeyError:
267+
raise KeyError(linkname)
268+
258269
for member in tar.getmembers():
259270
fn = member.name
260271
if leading:
@@ -269,6 +280,14 @@ def _untar_without_filter(
269280
if member.isdir():
270281
ensure_dir(path)
271282
elif member.issym():
283+
try:
284+
_check_link_target(tar, member)
285+
except KeyError as exc:
286+
message = (
287+
"The tar file ({}) has a file ({}) trying to install "
288+
"outside target directory ({})"
289+
)
290+
raise InstallationError(message.format(filename, member.name, exc))
272291
try:
273292
tar._extract_member(member, path)
274293
except Exception as exc:

tests/unit/test_utils_unpacking.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pathlib import Path
1111

1212
import pytest
13+
from _pytest.monkeypatch import MonkeyPatch
1314

1415
from pip._internal.exceptions import InstallationError
1516
from pip._internal.utils.unpacking import is_within_directory, untar_file, unzip_file
@@ -238,6 +239,105 @@ def test_unpack_tar_links(self, input_prefix: str, unpack_prefix: str) -> None:
238239
with open(os.path.join(unpack_dir, "symlink.txt"), "rb") as f:
239240
assert f.read() == content
240241

242+
def test_unpack_normal_tar_links_no_data_filter(
243+
self, monkeypatch: MonkeyPatch
244+
) -> None:
245+
"""
246+
Test unpacking a normal tar with file containing soft links, but no data_filter
247+
"""
248+
if hasattr(tarfile, "data_filter"):
249+
monkeypatch.delattr("tarfile.data_filter")
250+
251+
tar_filename = "test_tar_links_no_data_filter.tar"
252+
tar_filepath = os.path.join(self.tempdir, tar_filename)
253+
254+
extract_path = os.path.join(self.tempdir, "extract_path")
255+
256+
with tarfile.open(tar_filepath, "w") as tar:
257+
file_data = io.BytesIO(b"normal\n")
258+
normal_file_tarinfo = tarfile.TarInfo(name="normal_file")
259+
normal_file_tarinfo.size = len(file_data.getbuffer())
260+
tar.addfile(normal_file_tarinfo, fileobj=file_data)
261+
262+
info = tarfile.TarInfo("normal_symlink")
263+
info.type = tarfile.SYMTYPE
264+
info.linkpath = "normal_file"
265+
tar.addfile(info)
266+
267+
untar_file(tar_filepath, extract_path)
268+
269+
assert os.path.islink(os.path.join(extract_path, "normal_symlink"))
270+
271+
link_path = os.readlink(os.path.join(extract_path, "normal_symlink"))
272+
assert link_path == "normal_file"
273+
274+
with open(os.path.join(extract_path, "normal_symlink"), "rb") as f:
275+
assert f.read() == b"normal\n"
276+
277+
def test_unpack_evil_tar_link1_no_data_filter(
278+
self, monkeypatch: MonkeyPatch
279+
) -> None:
280+
"""
281+
Test unpacking a evil tar with file containing soft links, but no data_filter
282+
"""
283+
if hasattr(tarfile, "data_filter"):
284+
monkeypatch.delattr("tarfile.data_filter")
285+
286+
tar_filename = "test_tar_links_no_data_filter.tar"
287+
tar_filepath = os.path.join(self.tempdir, tar_filename)
288+
289+
import_filename = "import_file"
290+
import_filepath = os.path.join(self.tempdir, import_filename)
291+
open(import_filepath, "w").close()
292+
293+
extract_path = os.path.join(self.tempdir, "extract_path")
294+
295+
with tarfile.open(tar_filepath, "w") as tar:
296+
info = tarfile.TarInfo("evil_symlink")
297+
info.type = tarfile.SYMTYPE
298+
info.linkpath = import_filepath
299+
tar.addfile(info)
300+
301+
with pytest.raises(InstallationError) as e:
302+
untar_file(tar_filepath, extract_path)
303+
304+
assert "trying to install outside target directory" in str(e.value)
305+
assert "import_file" in str(e.value)
306+
307+
assert not os.path.exists(os.path.join(extract_path, "evil_symlink"))
308+
309+
def test_unpack_evil_tar_link2_no_data_filter(
310+
self, monkeypatch: MonkeyPatch
311+
) -> None:
312+
"""
313+
Test unpacking a evil tar with file containing soft links, but no data_filter
314+
"""
315+
if hasattr(tarfile, "data_filter"):
316+
monkeypatch.delattr("tarfile.data_filter")
317+
318+
tar_filename = "test_tar_links_no_data_filter.tar"
319+
tar_filepath = os.path.join(self.tempdir, tar_filename)
320+
321+
import_filename = "import_file"
322+
import_filepath = os.path.join(self.tempdir, import_filename)
323+
open(import_filepath, "w").close()
324+
325+
extract_path = os.path.join(self.tempdir, "extract_path")
326+
327+
with tarfile.open(tar_filepath, "w") as tar:
328+
info = tarfile.TarInfo("evil_symlink")
329+
info.type = tarfile.SYMTYPE
330+
info.linkpath = ".." + os.sep + import_filename
331+
tar.addfile(info)
332+
333+
with pytest.raises(InstallationError) as e:
334+
untar_file(tar_filepath, extract_path)
335+
336+
assert "trying to install outside target directory" in str(e.value)
337+
assert ".." + os.sep + import_filename in str(e.value)
338+
339+
assert not os.path.exists(os.path.join(extract_path, "evil_symlink"))
340+
241341

242342
def test_unpack_tar_unicode(tmpdir: Path) -> None:
243343
test_tar = tmpdir / "test.tar"

0 commit comments

Comments
 (0)