Skip to content

Commit 912eb7e

Browse files
committed
Merge branch 'TrellixVulnTeam/master'
PR #1990 * TrellixVulnTeam/master: Update tar validation to also check for abspath and symlinks Adding tarfile member sanitization to extractall()
2 parents 02cb1ab + 09ec745 commit 912eb7e

File tree

3 files changed

+83
-0
lines changed

3 files changed

+83
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "bugfix",
3+
"category": "tar",
4+
"description": "Validate tar extraction does not escape destination dir (#1990)"
5+
}

chalice/utils.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,60 @@ def extract_zipfile(self, zipfile_path: str, unpack_dir: str) -> None:
235235

236236
def extract_tarfile(self, tarfile_path: str, unpack_dir: str) -> None:
237237
with tarfile.open(tarfile_path, 'r:*') as tar:
238+
# In Python 3.12+, there's a `filter` arg where passing a
239+
# 'data' value will handle this behavior for us. To support older
240+
# versions of Python we handle this ourselves. We can't hook
241+
# into `extractall` directly so the idea is that we do a separate
242+
# validation pass first to ensure there's no files that try
243+
# to extract outside of the provided `unpack_dir`. This is roughly
244+
# based off of what's done in the `data_filter()` in Python 3.12.
245+
self._validate_safe_extract(tar, unpack_dir)
238246
tar.extractall(unpack_dir)
239247

248+
def _validate_safe_extract(
249+
self,
250+
tar: tarfile.TarFile,
251+
unpack_dir: str
252+
) -> None:
253+
for member in tar:
254+
self._validate_single_tar_member(member, unpack_dir)
255+
256+
def _validate_single_tar_member(
257+
self,
258+
member: tarfile.TarInfo,
259+
unpack_dir: str
260+
) -> None:
261+
name = member.name
262+
dest_path = os.path.realpath(unpack_dir)
263+
if name.startswith(('/', os.sep)):
264+
name = member.path.lstrip('/' + os.sep)
265+
if os.path.isabs(name):
266+
raise RuntimeError(f"Absolute path in tarfile not allowed: {name}")
267+
target_path = os.path.realpath(os.path.join(dest_path, name))
268+
# Check we don't escape the destination dir, e.g `../../foo`
269+
if os.path.commonpath([target_path, dest_path]) != dest_path:
270+
raise RuntimeError(
271+
f"Tar member outside destination dir: {target_path}")
272+
# If we're dealing with a member that's some type of link, ensure
273+
# it doesn't point to anything outside of the destination dir.
274+
if member.islnk() or member.issym():
275+
if os.path.abspath(member.linkname):
276+
raise RuntimeError(f"Symlink to abspath: {member.linkname}")
277+
if member.issym():
278+
target_path = os.path.join(
279+
dest_path,
280+
os.path.dirname(name),
281+
member.linkname,
282+
)
283+
else:
284+
target_path = os.path.join(
285+
dest_path,
286+
member.linkname)
287+
target_path = os.path.realpath(target_path)
288+
if os.path.commonpath([target_path, dest_path]) != dest_path:
289+
raise RuntimeError(
290+
f"Symlink outside of dest dir: {target_path}")
291+
240292
def directory_exists(self, path: str) -> bool:
241293
return os.path.isdir(path)
242294

tests/functional/test_utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import os
44
import io
5+
import tarfile
56

67
import pytest
78

@@ -108,6 +109,31 @@ def test_remove_stage_from_deployed_values_no_file(tmpdir):
108109
assert not os.path.isfile(filename)
109110

110111

112+
def test_error_raised_on_tar_out_of_extract_dir(tmp_path, osutils):
113+
filepath = tmp_path / 'badfile'
114+
filepath.write_text('single file')
115+
badtarpath = tmp_path / 'badtar.tar.gz'
116+
extractdir = tmp_path / 'nest1' / 'nest2' / 'nest3'
117+
with tarfile.open(badtarpath, 'w:gz') as tar:
118+
tar.add(filepath, arcname='../../escaped-dir.txt')
119+
with pytest.raises(RuntimeError):
120+
osutils.extract_tarfile(str(badtarpath), extractdir)
121+
122+
123+
def test_error_raise_tar_symlink_out_of_extract_dir(tmp_path, osutils):
124+
dir_with_symlink = tmp_path / 'nest1' / 'nest2'
125+
dir_with_symlink.mkdir(parents=True, exist_ok=True)
126+
outside_file = tmp_path / 'outside.txt'
127+
outside_file.write_text('outside of dir')
128+
symlink_file = dir_with_symlink / 'myfile.txt'
129+
os.symlink(outside_file, symlink_file)
130+
tarpath = dir_with_symlink / 'badtar.tar.gz'
131+
with tarfile.open(tarpath, 'w:gz') as tar:
132+
tar.add(symlink_file)
133+
with pytest.raises(RuntimeError):
134+
osutils.extract_tarfile(str(tarpath), dir_with_symlink)
135+
136+
111137
class TestOSUtils(object):
112138
def test_can_read_unicode(self, tmpdir, osutils):
113139
filename = str(tmpdir.join('file.txt'))

0 commit comments

Comments
 (0)