已合并
fix CVE-2026-82455 #348
tong_1001创建于 10 天前
fix CVE-2026-82455 #348
已合并
共 3 个文件变更+194-1
| @@ -0,0 +1,78 @@ | |||
| 1 | +From 643e0235fd7ea2bcb464c97eb2336a81d31b81b4 Mon Sep 17 00:00:00 2001 | ||
| 2 | +From: thesmartshadow <firaswq12@gmail.com> | ||
| 3 | +Date: Sun, 19 Apr 2026 02:20:37 +0300 | ||
| 4 | +Subject: [PATCH] Prevent symlink-based escape during gem extraction | ||
| 5 | + | ||
| 6 | +Reference:https://github.com/ruby/rubygems/commit/643e0235fd7ea2bcb464c97eb2336a81d31b81b4 | ||
| 7 | +Conflict:(1) delete `omit "Symlinks not supported or not enabled" unless symlink_supported?` in testcase | ||
| 8 | +(2) Define `destination_dir` at the start of the function, as seen in commits | ||
| 9 | +https://github.com/ruby/rubygems/commit/58173ff2eaae7dbc50da1b3ac66a604637b90ec9 | ||
| 10 | +and https://github.com/ruby/rubygems/commit/ac3b85bd5e90d003aa2d9296aec0bc48b5728f1 | ||
| 11 | +--- | ||
| 12 | + lib/rubygems/package.rb | 7 +++++++ | ||
| 13 | + test/rubygems/test_gem_package.rb | 24 ++++++++++++++++++++++++ | ||
| 14 | + 2 files changed, 31 insertions(+) | ||
| 15 | + | ||
| 16 | +diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb | ||
| 17 | +index 4672866985f8..2591a349f947 100644 | ||
| 18 | +--- a/lib/rubygems/package.rb | ||
| 19 | ++++ b/lib/rubygems/package.rb | ||
| 20 | + EOM | ||
| 21 | + # extracted. | ||
| 22 | + | ||
| 23 | + def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc: | ||
| 24 | ++ destination_dir = File.realpath(destination_dir) | ||
| 25 | ++ | ||
| 26 | + directories = [] | ||
| 27 | + symlinks = [] | ||
| 28 | + | ||
| 29 | + EOM | ||
| 30 | + directories << mkdir | ||
| 31 | + end | ||
| 32 | + | ||
| 33 | ++ real_mkdir = File.realpath(mkdir) | ||
| 34 | ++ unless real_mkdir == destination_dir || normalize_path(real_mkdir).start_with?(normalize_path(destination_dir + "/")) | ||
| 35 | ++ raise Gem::Package::PathError.new(real_mkdir, destination_dir) | ||
| 36 | ++ end | ||
| 37 | ++ | ||
| 38 | + if entry.file? | ||
| 39 | + File.open(destination, "wb") {|out| out.write entry.read } | ||
| 40 | + FileUtils.chmod file_mode(entry.header.mode), destination | ||
| 41 | +diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb | ||
| 42 | +index 9d6215838d63..b228a6170537 100644 | ||
| 43 | +--- a/test/rubygems/test_gem_package.rb | ||
| 44 | ++++ b/test/rubygems/test_gem_package.rb | ||
| 45 | + class TestGemPackage < Gem::Package::TarTestCase | ||
| 46 | + "#{@destination} is not allowed", e.message) | ||
| 47 | + end | ||
| 48 | + | ||
| 49 | ++ def test_extract_tar_gz_rejects_preexisting_symlink_escape | ||
| 50 | ++ package = Gem::Package.new @gem | ||
| 51 | ++ | ||
| 52 | ++ tgz_io = util_tar_gz do |tar| | ||
| 53 | ++ tar.add_file "lib/owned.txt", 0o644 do |io| | ||
| 54 | ++ io.write "poc-content" | ||
| 55 | ++ end | ||
| 56 | ++ end | ||
| 57 | ++ | ||
| 58 | ++ escape_dir = File.join(@tempdir, "escape") | ||
| 59 | ++ FileUtils.mkdir_p escape_dir | ||
| 60 | ++ | ||
| 61 | ++ FileUtils.rm_rf File.join(@destination, "lib") | ||
| 62 | ++ File.symlink escape_dir, File.join(@destination, "lib") | ||
| 63 | ++ | ||
| 64 | ++ escaped = File.join(escape_dir, "owned.txt") | ||
| 65 | ++ | ||
| 66 | ++ assert_raise Gem::Package::PathError do | ||
| 67 | ++ package.extract_tar_gz tgz_io, @destination | ||
| 68 | ++ end | ||
| 69 | ++ | ||
| 70 | ++ refute File.exist?(escaped), "must not write outside extraction root via symlink" | ||
| 71 | ++ end | ||
| 72 | ++ | ||
| 73 | + def test_extract_tar_gz_symlink_relative_path | ||
| 74 | + package = Gem::Package.new @gem | ||
| 75 | + package.verify | ||
| 76 | +-- | ||
| 77 | +2.43.0 | ||
| 78 | + | ||
| @@ -0,0 +1,107 @@ | |||
| 1 | +From 20e729f9a5d52aaa5a01aa4712ebfeb3ed6c2db0 Mon Sep 17 00:00:00 2001 | ||
| 2 | +From: Hiroshi SHIBATA <hsbt@ruby-lang.org> | ||
| 3 | +Date: Mon, 3 Aug 2026 11:38:39 +0900 | ||
| 4 | +Subject: [PATCH] Check the resolved parent directory before extracting old | ||
| 5 | + format gems | ||
| 6 | + | ||
| 7 | +Gem::Package::Old#extract_files only validated entry paths by string | ||
| 8 | +expansion, so a preexisting symlink in the extraction directory redirected | ||
| 9 | +writes outside of it. Gem::Package#extract_tar_gz already re-resolves the | ||
| 10 | +parent directory with File.realpath, so extract the shared check into | ||
| 11 | +verify_extraction_dir and call it from both. | ||
| 12 | + | ||
| 13 | +Reference:https://github.com/ruby/rubygems/commit/20e729f9a5d52aaa5a01aa4712ebfeb3ed6c2db0 | ||
| 14 | +Conflict:delete `omit "Symlinks not supported or not enabled" unless symlink_supported?` in testcase | ||
| 15 | + | ||
| 16 | +Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> | ||
| 17 | +--- | ||
| 18 | + lib/rubygems/package.rb | 19 +++++++++++++++---- | ||
| 19 | + lib/rubygems/package/old.rb | 6 ++++-- | ||
| 20 | + test/rubygems/test_gem_package_old.rb | 15 +++++++++++++++ | ||
| 21 | + 3 files changed, 34 insertions(+), 6 deletions(-) | ||
| 22 | + | ||
| 23 | +diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb | ||
| 24 | +index cb78a7d15c9d..1cc550a431c8 100644 | ||
| 25 | +--- a/lib/rubygems/package.rb | ||
| 26 | ++++ b/lib/rubygems/package.rb | ||
| 27 | + EOM | ||
| 28 | + directories << mkdir | ||
| 29 | + end | ||
| 30 | + | ||
| 31 | +- real_mkdir = File.realpath(mkdir) | ||
| 32 | +- unless real_mkdir == destination_dir || normalize_path(real_mkdir).start_with?(normalize_path(destination_dir + "/")) | ||
| 33 | +- raise Gem::Package::PathError.new(real_mkdir, destination_dir) | ||
| 34 | +- end | ||
| 35 | ++ verify_extraction_dir mkdir, destination_dir | ||
| 36 | + | ||
| 37 | + if entry.file? | ||
| 38 | + File.open(destination, "wb") {|out| out.write entry.read } | ||
| 39 | + EOM | ||
| 40 | + raise Gem::Package::FormatError.new e.message, @gem | ||
| 41 | + end | ||
| 42 | + | ||
| 43 | ++ ## | ||
| 44 | ++ # Raises an exception unless +dir+, with symlinks resolved, is | ||
| 45 | ++ # +destination_dir+ or a directory inside it. +destination_dir+ must | ||
| 46 | ++ # already be resolved with File.realpath by the caller. | ||
| 47 | ++ | ||
| 48 | ++ def verify_extraction_dir(dir, destination_dir) # :nodoc: | ||
| 49 | ++ real_dir = File.realpath(dir) | ||
| 50 | ++ | ||
| 51 | ++ return if real_dir == destination_dir || | ||
| 52 | ++ normalize_path(real_dir).start_with?(normalize_path(destination_dir + "/")) | ||
| 53 | ++ | ||
| 54 | ++ raise Gem::Package::PathError.new(real_dir, destination_dir) | ||
| 55 | ++ end | ||
| 56 | ++ | ||
| 57 | + ## | ||
| 58 | + # Verifies the +checksums+ against the +digests+. This check is not | ||
| 59 | + # cryptographically secure. Missing checksums are ignored. | ||
| 60 | +diff --git a/lib/rubygems/package/old.rb b/lib/rubygems/package/old.rb | ||
| 61 | +index 09a02d3ecd62..0b0b3b643199 100644 | ||
| 62 | +--- a/lib/rubygems/package/old.rb | ||
| 63 | ++++ b/lib/rubygems/package/old.rb | ||
| 64 | + class Gem::Package::Old < Gem::Package | ||
| 65 | + raise Gem::Package::FormatError, "#{full_name} in #{@gem} is corrupt" if | ||
| 66 | + file_data.length != entry["size"].to_i | ||
| 67 | + | ||
| 68 | +- FileUtils.rm_rf destination | ||
| 69 | +- | ||
| 70 | + FileUtils.mkdir_p File.dirname(destination), :mode => dir_mode && 0755 | ||
| 71 | + | ||
| 72 | ++ verify_extraction_dir File.dirname(destination), destination_dir | ||
| 73 | ++ | ||
| 74 | ++ FileUtils.rm_rf destination | ||
| 75 | ++ | ||
| 76 | + File.open destination, "wb", file_mode(entry["mode"]) do |out| | ||
| 77 | + out.write file_data | ||
| 78 | + end | ||
| 79 | +diff --git a/test/rubygems/test_gem_package_old.rb b/test/rubygems/test_gem_package_old.rb | ||
| 80 | +index d65d1edad65a..b232f9712f99 100644 | ||
| 81 | +--- a/test/rubygems/test_gem_package_old.rb | ||
| 82 | ++++ b/test/rubygems/test_gem_package_old.rb | ||
| 83 | + unless Gem.java_platform? # jruby can't require the simple_gem file | ||
| 84 | + assert_equal mask, File.stat(extracted).mode unless win_platform? | ||
| 85 | + end | ||
| 86 | + | ||
| 87 | ++ def test_extract_files_rejects_preexisting_symlink_escape | ||
| 88 | ++ | ||
| 89 | ++ escape_dir = File.join @tempdir, "escape" | ||
| 90 | ++ FileUtils.mkdir_p escape_dir | ||
| 91 | ++ | ||
| 92 | ++ File.symlink escape_dir, File.join(@destination, "lib") | ||
| 93 | ++ | ||
| 94 | ++ assert_raise Gem::Package::PathError do | ||
| 95 | ++ @package.extract_files @destination | ||
| 96 | ++ end | ||
| 97 | ++ | ||
| 98 | ++ assert_path_not_exist File.join(escape_dir, "foo.rb"), | ||
| 99 | ++ "must not write outside extraction root via symlink" | ||
| 100 | ++ end | ||
| 101 | ++ | ||
| 102 | + def test_extract_files_security_policy | ||
| 103 | + pend "openssl is missing" unless Gem::HAVE_OPENSSL | ||
| 104 | + | ||
| 105 | +-- | ||
| 106 | +2.43.0 | ||
| 107 | + | ||
| @@ -38,7 +38,7 @@ | |||
| 38 | 38 | ||
| 39 | Name: ruby | 39 | Name: ruby |
| 40 | Version: %{ruby_version} | 40 | Version: %{ruby_version} |
| 41 | -Release: 162 | 41 | +Release: 163 |
| 42 | Summary: Object-oriented scripting language interpreter | 42 | Summary: Object-oriented scripting language interpreter |
| 43 | License: (Ruby OR BSD-2-Clause) AND (Ruby OR BSD-2-Clause OR GPL-1.0-or-later) AND BSD-3-Clause AND (GPL-3.0-or-later WITH Bison-exception-2.2) AND ISC AND Public Domain AND MIT AND CC0 AND zlib AND Unicode-DFS-2015 | 43 | License: (Ruby OR BSD-2-Clause) AND (Ruby OR BSD-2-Clause OR GPL-1.0-or-later) AND BSD-3-Clause AND (GPL-3.0-or-later WITH Bison-exception-2.2) AND ISC AND Public Domain AND MIT AND CC0 AND zlib AND Unicode-DFS-2015 |
| 44 | URL: https://www.ruby-lang.org/en/ | 44 | URL: https://www.ruby-lang.org/en/ |
| @@ -153,6 +153,8 @@ Patch6062: backport-0001-CVE-2026-47242-Refactor-RawText-add-improve-test-covera | |||
| 153 | Patch6063: backport-0002-CVE-2026-47242-Validate-QuotedString-contains-only-valid-bytes.patch | 153 | Patch6063: backport-0002-CVE-2026-47242-Validate-QuotedString-contains-only-valid-bytes.patch |
| 154 | Patch6064: backport-CVE-2026-80212.patch | 154 | Patch6064: backport-CVE-2026-80212.patch |
| 155 | Patch6065: backport-CVE-2026-80213.patch | 155 | Patch6065: backport-CVE-2026-80213.patch |
| 156 | +Patch6066: backport-CVE-2026-82455.patch | ||
| 157 | +Patch6067: backport-Check-the-resolved-parent-directory-before-extractin.patch | ||
| 156 | 158 | ||
| 157 | Patch9000: fix-dynamic-constant-assignment-SyntaxError.patch | 159 | Patch9000: fix-dynamic-constant-assignment-SyntaxError.patch |
| 158 | 160 | ||
| @@ -964,6 +966,12 @@ make runruby TESTRUN_SCRIPT=%{SOURCE13} | |||
| 964 | %{gem_dir}/specifications/matrix-%{matrix_version}.gemspec | 966 | %{gem_dir}/specifications/matrix-%{matrix_version}.gemspec |
| 965 | 967 | ||
| 966 | %changelog | 968 | %changelog |
| 969 | +* Mon Aug 31 2026 shixuantong <sxt1001@qq.com> - 3.2.8-163 | ||
| 970 | +- Type:CVE | ||
| 971 | +- CVE:CVE-2026-82455 | ||
| 972 | +- SUG:NA | ||
| 973 | +- DESC:fix CVE-2026-82455 | ||
| 974 | + | ||
| 967 | * Sat Aug 29 2026 shixuantong <sxt1001@qq.com> - 3.2.8-162 | 975 | * Sat Aug 29 2026 shixuantong <sxt1001@qq.com> - 3.2.8-162 |
| 968 | - Type:CVE | 976 | - Type:CVE |
| 969 | - CVE:CVE-2026-80212 CVE-2026-80213 | 977 | - CVE:CVE-2026-80212 CVE-2026-80213 |