diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index 89aa84a1c733..d36b6dfb5e9a 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -453,6 +453,11 @@ def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc: 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") do |out| copy_stream(tar.io, out, entry.size) diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb index 43d4a07bd496..0014c2073727 100644 --- a/test/rubygems/test_gem_package.rb +++ b/test/rubygems/test_gem_package.rb @@ -615,6 +615,32 @@ def test_extract_tar_gz_symlink_directory File.read(extracted) end + def test_extract_tar_gz_rejects_preexisting_symlink_escape + omit "Symlinks not supported or not enabled" unless symlink_supported? + + 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_symlink_into_symlink_dir omit "Symlinks not supported or not enabled" unless symlink_supported? package = Gem::Package.new @gem