[rubygems/rubygems] Add a limit to the size of the metadata and checksums files in a gem package.

This is to prevent a malicious gem from causing a denial of service by
including a very large metadata or checksums file,
which is then read into memory in its entirety just by opening the gem package.

This is guaranteed to limit the amount of memory needed, since
gzips (which use deflate streams for compression) have a maximum compression
ratio of 1032:1, so the uncompressed size of the metadata or checksums file
will be at most 1032 times the size of the (limited) amount of data read.

This prevents a gem from causing 500GB of memory to be allocated
to read a 500MB metadata file.

https://github.com/rubygems/rubygems/commit/a596e3c5ec
This commit is contained in:
Samuel Giddins 2024-04-11 00:05:42 -07:00 committed by git
parent e0949c3f7c
commit d950609ec7

View File

@ -527,12 +527,13 @@ EOM
# Loads a Gem::Specification from the TarEntry +entry+ # Loads a Gem::Specification from the TarEntry +entry+
def load_spec(entry) # :nodoc: def load_spec(entry) # :nodoc:
limit = 10 * 1024 * 1024
case entry.full_name case entry.full_name
when "metadata" then when "metadata" then
@spec = Gem::Specification.from_yaml entry.read @spec = Gem::Specification.from_yaml limit_read(entry, "metadata", limit)
when "metadata.gz" then when "metadata.gz" then
Zlib::GzipReader.wrap(entry, external_encoding: Encoding::UTF_8) do |gzio| Zlib::GzipReader.wrap(entry, external_encoding: Encoding::UTF_8) do |gzio|
@spec = Gem::Specification.from_yaml gzio.read @spec = Gem::Specification.from_yaml limit_read(gzio, "metadata.gz", limit)
end end
end end
end end
@ -556,7 +557,7 @@ EOM
@checksums = gem.seek "checksums.yaml.gz" do |entry| @checksums = gem.seek "checksums.yaml.gz" do |entry|
Zlib::GzipReader.wrap entry do |gz_io| Zlib::GzipReader.wrap entry do |gz_io|
Gem::SafeYAML.safe_load gz_io.read Gem::SafeYAML.safe_load limit_read(gz_io, "checksums.yaml.gz", 10 * 1024 * 1024)
end end
end end
end end
@ -663,7 +664,7 @@ EOM
case file_name case file_name
when /\.sig$/ then when /\.sig$/ then
@signatures[$`] = entry.read if @security_policy @signatures[$`] = limit_read(entry, file_name, 1024 * 1024) if @security_policy
return return
else else
digest entry digest entry
@ -714,7 +715,7 @@ EOM
raise Gem::Package::FormatError.new(e.message, entry.full_name) raise Gem::Package::FormatError.new(e.message, entry.full_name)
end end
if RUBY_ENGINE == "truffleruby" if RUBY_ENGINE == "truffleruby" && RUBY_ENGINE_VERSION < "23.1.2"
def copy_stream(src, dst) # :nodoc: def copy_stream(src, dst) # :nodoc:
dst.write src.read dst.write src.read
end end
@ -723,6 +724,12 @@ EOM
IO.copy_stream(src, dst) IO.copy_stream(src, dst)
end end
end end
def limit_read(io, name, limit)
bytes = io.read(limit + 1)
raise Gem::Package::FormatError, "#{name} is too big (over #{limit} bytes)" if bytes.size > limit
bytes
end
end end
require_relative "package/digest_io" require_relative "package/digest_io"