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(+)

diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb
index 4672866985f8..2591a349f947 100644
--- a/lib/rubygems/package.rb
+++ b/lib/rubygems/package.rb
@@ -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
diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb
index 9d6215838d63..b228a6170537 100644
--- a/test/rubygems/test_gem_package.rb
+++ b/test/rubygems/test_gem_package.rb
@@ -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