Skip to content

Commit ae4c7dc

Browse files
MNT Fix path traversal issue in archive extraction (#4924)
The `_is_attempting_path_traversal` function did not properly handle absolute paths, allowing a maliciously crafted archive to write files outside of the intended extraction directory. This patch fixes the issue by: - Adding a check to explicitly disallow absolute paths in archive members. - Improving the relative path traversal check to ensure that the resolved path of an archive member is always within the intended output directory. b/431292024 --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 25a9973 commit ae4c7dc

File tree

2 files changed

+37
-11
lines changed

2 files changed

+37
-11
lines changed

src/clusterfuzz/_internal/system/archive.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,20 @@ def _is_attempting_path_traversal(archive_name: StrBytesPathLike,
6565
Whether there is a path traversal attempt
6666
"""
6767
output_dir = os.path.realpath(output_dir)
68-
absolute_file_path = os.path.join(output_dir, os.path.normpath(filename))
69-
real_file_path = os.path.realpath(absolute_file_path)
70-
71-
if real_file_path == output_dir:
72-
# Workaround for https://bugs.python.org/issue28488.
73-
# Ignore directories named '.'.
74-
return False
75-
76-
if real_file_path != absolute_file_path:
68+
if os.path.isabs(filename):
7769
logs.error('Directory traversal attempted while unpacking archive %s '
78-
'(file path=%s, actual file path=%s). Aborting.' %
79-
(archive_name, absolute_file_path, real_file_path))
70+
'(absolute file path=%s). Aborting.' % (archive_name, filename))
8071
return True
72+
73+
real_file_path = os.path.realpath(os.path.join(output_dir, filename))
74+
if (not real_file_path.startswith(output_dir + os.sep) and
75+
real_file_path != output_dir):
76+
logs.error(
77+
'Directory traversal attempted while unpacking archive %s '
78+
'(file path=%s, real file path=%s). Aborting.' %
79+
(archive_name, os.path.join(output_dir, filename), real_file_path))
80+
return True
81+
8182
return False
8283

8384

src/clusterfuzz/_internal/tests/core/system/archive_test.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
"""archive tests."""
15+
import io
1516
import os
17+
import tarfile
1618
import tempfile
1719
import unittest
1820

@@ -51,6 +53,29 @@ def test_file_list(self):
5153
[f.name for f in reader.list_members()],
5254
["archive_dir", "archive_dir/bye", "archive_dir/hi"])
5355

56+
def test_unpack_absolute_path_traversal(self):
57+
"""Test that unpacking an archive with an absolute path fails."""
58+
# Create a temporary TAR archive file on disk.
59+
with tempfile.NamedTemporaryFile(suffix='.tar') as tmp_tar_file:
60+
malicious_archive_path = tmp_tar_file.name
61+
62+
# Create a malicious archive with an absolute path payload.
63+
with tarfile.open(malicious_archive_path, 'w') as tar:
64+
file_data = b'malicious content'
65+
# Creating a TarInfo object with an absolute path.
66+
tarinfo = tarfile.TarInfo(name='/tmp/pwned')
67+
tarinfo.size = len(file_data)
68+
tar.addfile(tarinfo, io.BytesIO(file_data))
69+
70+
output_directory = tempfile.mkdtemp()
71+
72+
# The function should return False, indicating an error occurred.
73+
with archive.open(malicious_archive_path) as reader:
74+
result = reader.extract_all(output_directory, trusted=False)
75+
self.assertFalse(result)
76+
77+
shell.remove_directory(output_directory)
78+
5479

5580
class ArchiveReaderTest(unittest.TestCase):
5681
"""Tests for the archive.iterator function."""

0 commit comments

Comments
 (0)