Skip to content

Add safe extraction with permission handling for archives#283

Closed
bjk7119 wants to merge 3 commits into
mainfrom
chmod
Closed

Add safe extraction with permission handling for archives#283
bjk7119 wants to merge 3 commits into
mainfrom
chmod

Conversation

@bjk7119

@bjk7119 bjk7119 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • Bug Fixes
    • Archive extraction is safer and more reliable: skips path-traversal entries, normalizes file and directory permissions, and retries automatically on permission errors.
    • These improvements apply across multiple archive formats and detection paths, reducing failed or partial extractions.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds safe tar extraction helpers that compute safe member paths, normalize extracted file/directory permissions, and retry extraction on PermissionError; updates all tar extraction sites in the module to use the new safe extractor.

Changes

Safer tar extraction with permission handling

Layer / File(s) Summary
Tar extraction helper implementation
src/fosslight_util/download.py
Adds _member_extracted_paths(), _fix_extracted_permissions(), and _tar_extractall_safe() to record members, skip path-traversal members, normalize permissions (dirs 0o755, files 0o644), and retry extraction on PermissionError.
Apply safe extraction across extraction paths
src/fosslight_util/download.py
Replaces direct tarfile.extractall() calls with _tar_extractall_safe() for *.crate extraction, .tar.gz/.tgz, .tar.xz/.tar, and the magic-bytes tar detection branch.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding safe extraction utilities with permission handling for archive files, which is the core focus of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chmod

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/fosslight_util/download.py (1)

1116-1118: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

.crate extraction still bypasses the safe tar wrapper.

This branch still uses direct tarfile.extractall(), so it misses the new permission-retry and post-fix behavior. It should route through _tar_extractall_safe() like the other tar paths.

🔧 Suggested patch
             elif fname.endswith(".crate"):
-                with contextlib.closing(tarfile.open(fname, "r:gz")) as t:
-                    t.extractall(path=extract_path)
+                _tar_extractall_safe(fname, "r:gz", extract_path)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_util/download.py` around lines 1116 - 1118, The .crate branch
currently calls tarfile.extractall directly and should instead use the safe
extractor; replace the with contextlib.closing(tarfile.open(fname, "r:gz")) as
t: t.extractall(path=extract_path call with a call to _tar_extractall_safe(t,
extract_path) so the fname.endswith(".crate") branch reuses the existing
_tar_extractall_safe function (keep the tarfile.open(...) context wrapper and
variables fname and extract_path).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/fosslight_util/download.py`:
- Around line 1016-1034: _fix_extracted_permissions currently chmods every file
under the extraction root; change it to accept (or compute) the set of paths
extracted by the current archive and only normalize permissions for those files
and their ancestor directories. Specifically: update
_fix_extracted_permissions(path: str) to take an additional parameter like
extracted_paths: Iterable[str] (or compute a set inside the caller), build a
canonical set of absolute file paths and their parent directories under the
given extract_path, then when walking (or when handling the initial path) only
call os.chmod for entries whose absolute path is in that allowed set; apply the
same change to the duplicate logic around lines 1047-1060 so pre-existing files
outside the current archive are not modified. Ensure you reference and update
callers that invoke _fix_extracted_permissions to provide the extracted paths
set.
- Around line 1039-1040: Replace all raw tarfile.extractall(...) calls with the
safe extractor _tar_extractall_safe() (including the .crate extraction branch
currently calling t.extractall(path=extract_path)) so every extraction path
enforces path traversal checks; implement in _tar_extractall_safe() a
Python-3.10-safe fallback that, when tarfile.extractall(filter=...) is
unavailable, iterates members, computes target =
os.path.normpath(os.path.join(extract_path, member.name)), verifies
target.startswith(os.path.abspath(extract_path) + os.sep) before extracting that
member (and skips/raises on violations); and modify _fix_extracted_permissions()
to only chmod existing files/directories and surface or log permission errors
instead of silently swallowing exceptions. Ensure all references to
tarfile.extractall in this module are routed through the updated
_tar_extractall_safe().

---

Outside diff comments:
In `@src/fosslight_util/download.py`:
- Around line 1116-1118: The .crate branch currently calls tarfile.extractall
directly and should instead use the safe extractor; replace the with
contextlib.closing(tarfile.open(fname, "r:gz")) as t:
t.extractall(path=extract_path call with a call to _tar_extractall_safe(t,
extract_path) so the fname.endswith(".crate") branch reuses the existing
_tar_extractall_safe function (keep the tarfile.open(...) context wrapper and
variables fname and extract_path).
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 89d6b2b8-0192-40ae-9671-7ed631ff1783

📥 Commits

Reviewing files that changed from the base of the PR and between a71ee67 and 0efabbc.

📒 Files selected for processing (1)
  • src/fosslight_util/download.py

Comment thread src/fosslight_util/download.py Outdated
Comment thread src/fosslight_util/download.py Outdated
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • src/fosslight_util/download.py

Commit: a8317a76a2579430cacacfd36550034f12230933

The changes have been pushed to the chmod branch.

Time taken: 3m 34s

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/fosslight_util/download.py (2)

1039-1060: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include ancestor directories in the permission-retry set.

_fix_extracted_permissions() only receives explicit directory members plus file paths. If an archive contains pkg/file.txt without a separate pkg/ entry and extract_path/pkg is what caused the original PermissionError, this retry skips that parent directory, skips the still-missing file, and then fails again on the same member. Please add each file member's parent chain under extract_path to member_dirs before retrying.

🛠️ Minimal fix
 def _member_extracted_paths(members, extract_path: str) -> tuple[set, set]:
@@
                 if m.isdir():
                     dir_paths.add(abs_path)
                 elif m.isfile():
                     file_paths.add(abs_path)
+                    parent = os.path.dirname(abs_path)
+                    while os.path.commonpath([parent, abs_extract]) == abs_extract:
+                        dir_paths.add(parent)
+                        if parent == abs_extract:
+                            break
+                        parent = os.path.dirname(parent)
         except ValueError:
             pass

Also applies to: 1108-1109, 1137-1138

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_util/download.py` around lines 1039 - 1060, The retry logic in
_fix_extracted_permissions only iterates explicit member_dirs and skips parent
directories that may not be listed (e.g., archive has "pkg/file.txt" but no
"pkg/" entry), so before fixing permissions add each file member's parent chain
(up to the extraction root) into member_dirs: for every path in member_files,
walk its parents and insert them into the member_dirs set (ensuring you only add
paths under the intended extract root if available), then proceed with the
existing directory-first chmod loop; apply the same parent-chain addition to the
two other similar permission-fix blocks noted in the file.

1072-1097: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Manual fallback still permits unsafe tar member types.

The fallback only bounds-checks member.name and then calls tf.extract(member, ...). That still leaves the older-runtime branch accepting symlink/hardlink members whose link targets escape the destination, and special members like device/FIFO entries, so it is not equivalent to filter='data'. The stdlib data filter explicitly rejects links to absolute/out-of-destination targets and refuses special files. (docs.python.org)

Also applies to: 1119-1131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fosslight_util/download.py` around lines 1072 - 1097, The manual fallback
currently only bounds-checks member.name then calls tf.extract(member,...),
which still allows symlink/hardlink and special device/FIFO entries to escape or
be created; update the members loop (the branch where has_filter is False) to
reject non-regular file members: call
TarInfo.issym()/islnk()/isdev()/isfifo()/ischr()/isblk() on each member and
raise a ValueError for any non-regular type, and for symlink/hardlink also
validate member.linkname by resolving
os.path.normpath(os.path.join(extract_path, member.linkname)) to an absolute
path and ensuring it is inside abs_extract (or simply reject any link entries if
you prefer stricter behavior); only allow extraction (tf.extract or
tf.extractfile->write) for regular files and directories after these checks to
match the stdlib 'data' filter behavior (refer to variables: has_filter, tf,
members, member, extract_path, abs_extract, member.name, member.linkname).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/fosslight_util/download.py`:
- Around line 1039-1060: The retry logic in _fix_extracted_permissions only
iterates explicit member_dirs and skips parent directories that may not be
listed (e.g., archive has "pkg/file.txt" but no "pkg/" entry), so before fixing
permissions add each file member's parent chain (up to the extraction root) into
member_dirs: for every path in member_files, walk its parents and insert them
into the member_dirs set (ensuring you only add paths under the intended extract
root if available), then proceed with the existing directory-first chmod loop;
apply the same parent-chain addition to the two other similar permission-fix
blocks noted in the file.
- Around line 1072-1097: The manual fallback currently only bounds-checks
member.name then calls tf.extract(member,...), which still allows
symlink/hardlink and special device/FIFO entries to escape or be created; update
the members loop (the branch where has_filter is False) to reject non-regular
file members: call TarInfo.issym()/islnk()/isdev()/isfifo()/ischr()/isblk() on
each member and raise a ValueError for any non-regular type, and for
symlink/hardlink also validate member.linkname by resolving
os.path.normpath(os.path.join(extract_path, member.linkname)) to an absolute
path and ensuring it is inside abs_extract (or simply reject any link entries if
you prefer stricter behavior); only allow extraction (tf.extract or
tf.extractfile->write) for regular files and directories after these checks to
match the stdlib 'data' filter behavior (refer to variables: has_filter, tf,
members, member, extract_path, abs_extract, member.name, member.linkname).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9c91b8ff-c231-4284-be0c-edd9241cd7e2

📥 Commits

Reviewing files that changed from the base of the PR and between 4c0b50f and a8317a7.

📒 Files selected for processing (1)
  • src/fosslight_util/download.py

@bjk7119 bjk7119 closed this Jun 12, 2026
@bjk7119
bjk7119 deleted the chmod branch June 12, 2026 05:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant