Skip to content

✨ Return non-zero exit code when entries are skipped for path security#2839

Open
ChanTsune wants to merge 1 commit into
mainfrom
cli/extract/security-skip-exit-code
Open

✨ Return non-zero exit code when entries are skipped for path security#2839
ChanTsune wants to merge 1 commit into
mainfrom
cli/extract/security-skip-exit-code

Conversation

@ChanTsune

@ChanTsune ChanTsune commented Mar 26, 2026

Copy link
Copy Markdown
Owner

Add Outcome, FilterResult, and ExtractResult enums to the extract pipeline to distinguish security-related skips (path traversal, unsafe links) from normal filter skips (glob, exclude, overwrite).

When any entry is skipped due to a security violation, the extract command now returns exit code 1 with a warning on stderr, matching bsdtar's behavior of setting return_value=1 on ARCHIVE_WARN.

Update path_traversal integration tests to expect failure exit code when security skips occur.

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Archive extraction now properly detects and reports path traversal and security violations.
    • Extraction operations return a warning status when unsafe entries are encountered.
    • Unsafe symbolic links and hardlinks are blocked by default during extraction.
    • Updated tests confirm dangerous extraction scenarios are now prevented.

Add Outcome, FilterResult, and ExtractResult enums to the extract
pipeline to distinguish security-related skips (path traversal,
unsafe links) from normal filter skips (glob, exclude, overwrite).

When any entry is skipped due to a security violation, the extract
command now returns exit code 1 with a warning on stderr, matching
bsdtar's behavior of setting return_value=1 on ARCHIVE_WARN.

Update path_traversal integration tests to expect failure exit code
when security skips occur.
@coderabbitai

coderabbitai Bot commented Mar 26, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This pull request introduces security-aware outcome tracking to the extraction process. The extraction functions now return an Outcome enum to distinguish between successful extractions and those with security violations. When unsafe paths are detected (e.g., parent directory traversal or unsafe symlinks), extraction is recorded as a warning outcome, and commands abort with an appropriate error message.

Changes

Cohort / File(s) Summary
Extraction Security Tracking
cli/src/command/extract.rs
Added Outcome and ExtractResult enums to track extraction success vs. security warnings. Modified run_extract_archive_reader and run_extract_archive to return anyhow::Result<Outcome> instead of anyhow::Result<()>. Introduced FilterResult enum and security-skip detection via Arc<AtomicBool>. Updated filter_entry to detect unsafe paths (parent directory components) and extract_entry to return ExtractResult distinguishing SecuritySkipped from regular Skipped/Extracted states.
Outcome Handling
cli/src/command/bsdtar.rs
Updated run_extract_archive to capture extraction outcome and abort with anyhow::bail! when Outcome::Warn is returned, indicating path security violations.
Test Assertions
cli/tests/cli/stdio/path_traversal.rs
Inverted test expectations in four path traversal defense cases from assert().success() to assert().failure() for symlink/hardlink extraction blocked by security checks.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

cli

Poem

🐰 A rabbit hops through archives deep,
Now catching paths that shouldn't creep,
With parent dirs and symlinks sly—
Security warnings reach the sky!
Safe extraction, no traverse passed,
Our warren's archives locked up fast! 🔐

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 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: returning a non-zero exit code when entries are skipped due to path security violations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 cli/extract/security-skip-exit-code

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.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the security posture of the archive extraction process by introducing a clear signal for security-related skips. By returning a non-zero exit code and providing more detailed categorization of skipped entries, it improves the robustness and user feedback of the extraction command, making it easier to identify and react to potential security vulnerabilities during archive processing.

Highlights

  • Enhanced Security Exit Codes: The extract command now returns a non-zero exit code (1) when any entries are skipped due to security violations, such as path traversal or unsafe links. This aligns its behavior with bsdtar's ARCHIVE_WARN.
  • Granular Skip Categorization: New enums (Outcome, FilterResult, ExtractResult) have been introduced to clearly distinguish between different types of skipped entries: user-defined filter skips (glob, exclude, overwrite) and security-related skips.
  • Updated Integration Tests: Existing path traversal integration tests have been updated to correctly assert a failure() exit code when security-related skips occur, ensuring the new behavior is properly tested.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces enhanced security checks for path traversal vulnerabilities during archive extraction. The extraction logic has been refactored to return an Outcome enum, which indicates whether the extraction was successful or if security-related warnings occurred. New FilterResult and ExtractResult enums are introduced to differentiate between various reasons for skipping entries, including path traversal or unsafe link targets. An atomic boolean had_security_skip tracks security-related skips, and if any occur, the bsdtar and extract commands now bail out with an error. Corresponding unit tests have been updated to reflect these new failure conditions for path traversal scenarios. I have no feedback to provide.

@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: 1

Caution

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

⚠️ Outside diff range comments (2)
cli/src/command/extract.rs (2)

553-559: ⚠️ Potential issue | 🟡 Minor

Don't log success before handling Outcome::Warn.

A security-skip run now emits “Successfully extracted...” and then immediately returns an error, which makes the logs contradictory for the exact case this PR adds.

🐛 Proposed fix
-    log::info!(
-        "Successfully extracted an archive in {}",
-        DurationDisplay(start.elapsed())
-    );
     if outcome == Outcome::Warn {
         anyhow::bail!("One or more entries were not extracted due to path security violations");
     }
+    log::info!(
+        "Successfully extracted an archive in {}",
+        DurationDisplay(start.elapsed())
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/command/extract.rs` around lines 553 - 559, The success message is
logged before checking for Outcome::Warn, causing contradictory logs when
extraction is skipped for security reasons; modify the code around the
log::info! call and the Outcome::Warn check in extract.rs so that you only emit
the "Successfully extracted an archive in {}" message (using
DurationDisplay(start.elapsed())) when outcome != Outcome::Warn (or move the log
call to after the warn-check), and ensure that if outcome == Outcome::Warn you
bail with the existing anyhow::bail! without logging success.

1414-1420: ⚠️ Potential issue | 🟠 Major

Verify that edit_hardlink() misses are always security-related.

This branch unconditionally returns ExtractResult::SecuritySkipped, but edit_hardlink() can return None for benign cases. The transform_and_strip() function returns None when paths become empty after transformation or stripping (via is_effectively_empty_path() checks), which are non-security skips. Meanwhile, only the check_nodotdot() check (which guards against .. components) is documented as security-related. Map empty-path cases to ExtractResult::Skipped and reserve SecuritySkipped for actual unsafe-target rejections like parent-directory traversal.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/command/extract.rs` around lines 1414 - 1420, The current branch
treats every None from pathname_editor.edit_hardlink(...) as SecuritySkipped,
but edit_hardlink can return None for benign empty-path cases (from
transform_and_strip/is_effectively_empty_path()) as well as true security
rejects (check_nodotdot()); update the logic to distinguish those: modify
edit_hardlink (or add a new helper) to return a richer result (e.g., enum {
Ok((PathBuf,bool)), SkippedEmpty, SecurityRejected }) or call
transform_and_strip/check_nodotdot before deciding, and then map SkippedEmpty
(or empty-path detection via is_effectively_empty_path()/transform_and_strip())
to ExtractResult::Skipped while mapping actual unsafe-target/nodotdot rejections
to ExtractResult::SecuritySkipped; ensure references to edit_hardlink,
transform_and_strip, is_effectively_empty_path, check_nodotdot, and the
ExtractResult variants are used so reviewers can find the changes.
🧹 Nitpick comments (1)
cli/tests/cli/stdio/path_traversal.rs (1)

147-148: Assert the exact warning-exit contract here.

.failure() will also pass on unrelated non-zero exits or a panic. Since these cases are the regression coverage for the new security-skip behavior, please pin them to exit code 1 and stderr containing the path-security message as well.

Also applies to: 339-340, 384-385, 565-566

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/tests/cli/stdio/path_traversal.rs` around lines 147 - 148, Replace the
loose .assert().failure() checks in the path traversal tests with a strict
assertion that the process exited with code 1 and stderr contains the
path-security warning; specifically, change occurrences of the
.assert().failure() call (e.g., the chain using .assert() in path_traversal.rs)
to something like
.assert().code(1).stderr(predicate::str::contains("path-security") /* or the
exact warning text */) so the test pins both the exit code and the error
message; apply the same replacement to the other occurrences flagged in the
comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/src/command/extract.rs`:
- Around line 1105-1117: Don't re-infer skip reason from the raw archive path;
instead use the pathname editor's richer result. Replace the current let
Some(name) = args.pathname_editor.edit_entry_name(...) else { ... } block with a
call that captures the editor's full result (e.g., use the PathnameEditor
method/return type that includes a reason or status instead of just Option — the
existing edit_entry_name variant that yields a richer enum/struct in
cli/src/command/core/path.rs), then map its variants to
FilterResult::SecuritySkipped or FilterResult::Skipped based on the
editor-provided reason (preserve logging messages), and apply the same change
for the second occurrence around lines 1182-1193; reference symbols:
args.pathname_editor, edit_entry_name (or the richer edit result API), and
FilterResult::{SecuritySkipped, Skipped}.

---

Outside diff comments:
In `@cli/src/command/extract.rs`:
- Around line 553-559: The success message is logged before checking for
Outcome::Warn, causing contradictory logs when extraction is skipped for
security reasons; modify the code around the log::info! call and the
Outcome::Warn check in extract.rs so that you only emit the "Successfully
extracted an archive in {}" message (using DurationDisplay(start.elapsed()))
when outcome != Outcome::Warn (or move the log call to after the warn-check),
and ensure that if outcome == Outcome::Warn you bail with the existing
anyhow::bail! without logging success.
- Around line 1414-1420: The current branch treats every None from
pathname_editor.edit_hardlink(...) as SecuritySkipped, but edit_hardlink can
return None for benign empty-path cases (from
transform_and_strip/is_effectively_empty_path()) as well as true security
rejects (check_nodotdot()); update the logic to distinguish those: modify
edit_hardlink (or add a new helper) to return a richer result (e.g., enum {
Ok((PathBuf,bool)), SkippedEmpty, SecurityRejected }) or call
transform_and_strip/check_nodotdot before deciding, and then map SkippedEmpty
(or empty-path detection via is_effectively_empty_path()/transform_and_strip())
to ExtractResult::Skipped while mapping actual unsafe-target/nodotdot rejections
to ExtractResult::SecuritySkipped; ensure references to edit_hardlink,
transform_and_strip, is_effectively_empty_path, check_nodotdot, and the
ExtractResult variants are used so reviewers can find the changes.

---

Nitpick comments:
In `@cli/tests/cli/stdio/path_traversal.rs`:
- Around line 147-148: Replace the loose .assert().failure() checks in the path
traversal tests with a strict assertion that the process exited with code 1 and
stderr contains the path-security warning; specifically, change occurrences of
the .assert().failure() call (e.g., the chain using .assert() in
path_traversal.rs) to something like
.assert().code(1).stderr(predicate::str::contains("path-security") /* or the
exact warning text */) so the test pins both the exit code and the error
message; apply the same replacement to the other occurrences flagged in the
comment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a54cf243-eda8-4058-9a5e-121a43f5c690

📥 Commits

Reviewing files that changed from the base of the PR and between 4aeafa8 and b7b8cea.

📒 Files selected for processing (3)
  • cli/src/command/bsdtar.rs
  • cli/src/command/extract.rs
  • cli/tests/cli/stdio/path_traversal.rs

Comment on lines +1105 to +1117
let Some(name) = args.pathname_editor.edit_entry_name(item_name.as_path()) else {
// Distinguish security-related skips (path traversal) from benign skips
// (path became empty after sanitization, e.g. "." entries).
if item_name
.as_path()
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
log::warn!("Skipping entry with unsafe path: {item_name}");
return FilterResult::SecuritySkipped;
}
log::debug!("Skip: {item_name}");
}
name
return FilterResult::Skipped;

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.

⚠️ Potential issue | 🟠 Major

Don't infer the skip reason from the raw archive path.

Per cli/src/command/core/path.rs:43-55 and cli/src/command/core/path.rs:97-134, edit_entry_name() returns None for benign empty-after-strip / empty-after-sanitize cases as well as real .. rejections. Re-checking the pre-transform item_name here makes the new outcome unreliable: a/../b stripped away by --strip-components becomes a false SecuritySkipped, while a rename rule that introduces .. becomes a plain Skipped. This needs a richer reason from PathnameEditor instead of reconstructing it here.

Also applies to: 1182-1193

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/command/extract.rs` around lines 1105 - 1117, Don't re-infer skip
reason from the raw archive path; instead use the pathname editor's richer
result. Replace the current let Some(name) =
args.pathname_editor.edit_entry_name(...) else { ... } block with a call that
captures the editor's full result (e.g., use the PathnameEditor method/return
type that includes a reason or status instead of just Option — the existing
edit_entry_name variant that yields a richer enum/struct in
cli/src/command/core/path.rs), then map its variants to
FilterResult::SecuritySkipped or FilterResult::Skipped based on the
editor-provided reason (preserve logging messages), and apply the same change
for the second occurrence around lines 1182-1193; reference symbols:
args.pathname_editor, edit_entry_name (or the richer edit result API), and
FilterResult::{SecuritySkipped, Skipped}.

@github-actions github-actions Bot added the cli This issue is about cli application label Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli This issue is about cli application

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant