From 643e0235fd7ea2bcb464c97eb2336a81d31b81b4 Mon Sep 17 00:00:00 2001
From: thesmartshadow <firaswq12@gmail.com>
Date: Sun, 19 Apr 2026 02:20:37 +0300
Subject: [PATCH] Prevent symlink-based escape during gem extraction
Reference:https://github.com/ruby/rubygems/commit/643e0235fd7ea2bcb464c97eb2336a81d31b81b4
Conflict:(1) delete `omit "Symlinks not supported or not enabled" unless symlink_supported?` in testcase
(2) Define `destination_dir` at the start of the function, as seen in commits
https://github.com/ruby/rubygems/commit/58173ff2eaae7dbc50da1b3ac66a604637b90ec9
and https://github.com/ruby/rubygems/commit/ac3b85bd5e90d003aa2d9296aec0bc48b5728f1
lib/rubygems/package.rb | 7 +++++++
test/rubygems/test_gem_package.rb | 24 ++++++++++++++++++++++++
2 files changed, 31 insertions(+)
@@ -408,6 +408,8 @@ EOM
# extracted.
def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc:
+ destination_dir = File.realpath(destination_dir)
+
directories = []
symlinks = []
@@ -444,6 +446,11 @@ EOM
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
+
if entry.file?
File.open(destination, "wb") {|out| out.write entry.read }
FileUtils.chmod file_mode(entry.header.mode), destination
@@ -542,6 +542,30 @@ class TestGemPackage < Gem::Package::TarTestCase
"#{@destination} is not allowed", e.message)
end
+ def test_extract_tar_gz_rejects_preexisting_symlink_escape
+ package = Gem::Package.new @gem
+
+ tgz_io = util_tar_gz do |tar|
+ tar.add_file "lib/owned.txt", 0o644 do |io|
+ io.write "poc-content"
+ end
+ end
+
+ escape_dir = File.join(@tempdir, "escape")
+ FileUtils.mkdir_p escape_dir
+
+ FileUtils.rm_rf File.join(@destination, "lib")
+ File.symlink escape_dir, File.join(@destination, "lib")
+
+ escaped = File.join(escape_dir, "owned.txt")
+
+ assert_raise Gem::Package::PathError do
+ package.extract_tar_gz tgz_io, @destination
+ end
+
+ refute File.exist?(escaped), "must not write outside extraction root via symlink"
+ end
+
def test_extract_tar_gz_symlink_relative_path
package = Gem::Package.new @gem
package.verify
--
2.43.0