Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/source/releasenotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ Latest versions
`Upcoming release <https://github.com/robocorp/rpaframework/projects/3#column-16713994>`_
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

No changes planned yet for the next release.
- **Security:** ``RPA.Archive``: Fix a Zip Slip path traversal vulnerability (CWE-22) in
``Extract Archive`` — archive members with path traversal sequences (e.g.
``../../evil.py``) could previously be extracted outside the requested destination
directory. Extraction now validates that every member resolves within the destination
before extracting, raising ``ValueError`` otherwise (fixes :issue:`1339`, :issue:`1340`).

- ``rpaframework`` **32.0.2**

`Released <https://pypi.org/project/rpaframework/#history>`_
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Expand Down
2 changes: 1 addition & 1 deletion packages/main/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "rpaframework"
version = "32.0.1"
version = "32.0.2"
description = "A collection of tools and libraries for RPA"
authors = [{name = "RPA Framework", email = "rpafw@robocorp.com"}]
license = {text = "Apache-2.0"}
Expand Down
25 changes: 22 additions & 3 deletions packages/main/src/RPA/Archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ def convert_date(timestamp):
return formatted_date


def _check_extraction_safety(names, dest: Path) -> None:
"""Raise ValueError if extracting any of `names` under `dest` would escape it.

Guards against Zip Slip / path traversal (CWE-22) via archive members
containing sequences like ``../../evil.py``.
"""
dest = dest.resolve()
for name in names:
target = (dest / name).resolve()
if target != dest and dest not in target.parents:
raise ValueError(
f"Refusing to extract {name!r}: path escapes destination directory"
)


def list_files_in_directory(folder, recursive=False, include=None, exclude=None):
filelist = []
for rootdir, _, files in os.walk(folder):
Expand Down Expand Up @@ -373,15 +388,19 @@ def extract_archive(
members = [members]
if zipfile.is_zipfile(archive_name):
with zipfile.ZipFile(archive_name, "r") as f:
names = members if members else f.namelist()
_check_extraction_safety(names, root)
if members:
f.extractall(path=root, members=members)
else:
f.extractall(path=root)
elif tarfile.is_tarfile(archive_name):
members = map(tarfile.TarInfo, members) if members else None
with tarfile.open(archive_name, "r") as f:
if members:
f.extractall(path=root, members=members)
names = members if members else [m.name for m in f.getmembers()]
_check_extraction_safety(names, root)
tar_members = map(tarfile.TarInfo, members) if members else None
if tar_members:
f.extractall(path=root, members=tar_members)
else:
f.extractall(path=root)

Expand Down
61 changes: 61 additions & 0 deletions packages/main/tests/python/test_archive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import tarfile
import zipfile

import pytest
from RPA.Archive import Archive


@pytest.fixture
def lib():
return Archive()


def _make_traversal_zip(path):
with zipfile.ZipFile(path, "w") as zf:
zf.writestr("../evil_zip.txt", "pwned")
return path


def _make_traversal_tar(path):
with tarfile.open(path, "w") as tf:
info = tarfile.TarInfo(name="../evil_tar.txt")
data = b"pwned"
info.size = len(data)
import io

tf.addfile(info, io.BytesIO(data))
return path


def test_extract_archive_rejects_zip_slip(lib, tmp_path):
archive = _make_traversal_zip(tmp_path / "evil.zip")
extract_dir = tmp_path / "extracted"
extract_dir.mkdir()

with pytest.raises(ValueError):
lib.extract_archive(str(archive), str(extract_dir))

assert not (tmp_path / "evil_zip.txt").exists()


def test_extract_archive_rejects_tar_slip(lib, tmp_path):
archive = _make_traversal_tar(tmp_path / "evil.tar")
extract_dir = tmp_path / "extracted"
extract_dir.mkdir()

with pytest.raises(ValueError):
lib.extract_archive(str(archive), str(extract_dir))

assert not (tmp_path / "evil_tar.txt").exists()


def test_extract_archive_allows_normal_zip(lib, tmp_path):
archive = tmp_path / "ok.zip"
with zipfile.ZipFile(archive, "w") as zf:
zf.writestr("hello.txt", "hi")
extract_dir = tmp_path / "extracted"
extract_dir.mkdir()

lib.extract_archive(str(archive), str(extract_dir))

assert (extract_dir / "hello.txt").read_text() == "hi"
Loading