Skip to content

Prevent extraction from escaping destination_dir via pre-existing symlinks#9493

Open
thesmartshadow wants to merge 3 commits into
ruby:masterfrom
thesmartshadow:fix/cwe59-symlink-extract
Open

Prevent extraction from escaping destination_dir via pre-existing symlinks#9493
thesmartshadow wants to merge 3 commits into
ruby:masterfrom
thesmartshadow:fix/cwe59-symlink-extract

Conversation

@thesmartshadow
Copy link
Copy Markdown

What was the end-user or developer problem that led to this PR?

When extracting a gem, RubyGems correctly rejects obvious path traversal patterns such as absolute paths or .., but it can still follow a pre-existing symlink inside the destination directory.

In practice, if part of the extraction path already exists as a symlink pointing outside the extraction root, a file that appears to be written under destination_dir can end up being written outside of it instead. That breaks the expected safety boundary of extraction and makes extraction unsafe in reused or shared directories.

What is your fix for the problem, implemented in this PR?

This PR adds a link-aware safety check before writing extracted files.

After ensuring the parent directory exists, it resolves the real path of that parent directory and verifies that it still stays under the intended extraction root. If the resolved parent path escapes destination_dir, extraction now raises Gem::Package::PathError instead of continuing with the write.

This keeps extraction confined to the intended destination even when pre-existing symlinks are present in the path.

This PR also adds a regression test covering that case by planting a symlink in the destination path and asserting that:

  • extraction raises Gem::Package::PathError
  • no file is written outside the extraction root

Validation

I verified the change with:

  • the regression test added in this PR
  • the existing nearby symlink-related tests
  • a local reproducer that previously wrote outside the extraction root and now fails with Gem::Package::PathError

Checklist

  • Describe the problem / feature
  • Write tests for features and bug fixes
  • Write code to solve the problem
  • Make sure you follow the current code style and write meaningful commit messages without tags

Copy link
Copy Markdown
Member

@hsbt hsbt left a comment

Choose a reason for hiding this comment

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

Please align code indentation at first.

@thesmartshadow thesmartshadow force-pushed the fix/cwe59-symlink-extract branch from 2c5093b to 643e023 Compare May 1, 2026 18:16
@thesmartshadow
Copy link
Copy Markdown
Author

Please align code indentation at first.

Thanks I fixed the indentation and pushed an update.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens Gem::Package#extract_tar_gz to prevent extraction from escaping destination_dir when pre-existing symlinks exist inside the destination tree, and adds a regression test for that scenario.

Changes:

  • Add a realpath-based containment check on the target entry’s parent directory during extraction.
  • Add a regression test ensuring extraction fails when a pre-existing symlink in the destination path would redirect writes outside the extraction root.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
lib/rubygems/package.rb Adds a link-aware containment check intended to block symlink-based escape during extraction.
test/rubygems/test_gem_package.rb Adds a regression test that plants a symlink in the destination path and expects Gem::Package::PathError.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rubygems/package.rb
Comment on lines 451 to +459
unless directories.include?(mkdir)
FileUtils.mkdir_p mkdir, mode: dir_mode ? 0o755 : (entry.header.mode if entry.directory?)
directories << mkdir
end

real_mkdir = File.realpath(mkdir)
unless real_mkdir == destination_dir || normalize_path(real_mkdir).start_with?(normalize_path(destination_dir + "/"))
raise Gem::Package::PathError.new(real_mkdir, destination_dir)
end
Comment thread lib/rubygems/package.rb
raise Gem::Package::PathError.new(real_mkdir, destination_dir)
end

if entry.file?

refute File.exist?(escaped), "must not write outside extraction root via symlink"
end

Comment thread test/rubygems/test_gem_package.rb Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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.

3 participants