Skip to content

Commit

Permalink
Add a limit to the size of the metadata and checksums files in a gem …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
segiddins committed Apr 28, 2024
1 parent 9312624 commit a596e3c
Showing 1 changed file with 12 additions and 5 deletions.
17 changes: 12 additions & 5 deletions lib/rubygems/package.rb
Expand Up @@ -528,12 +528,13 @@ def normalize_path(pathname)
# Loads a Gem::Specification from the TarEntry +entry+

def load_spec(entry) # :nodoc:
limit = 10 * 1024 * 1024
case entry.full_name
when "metadata" then
@spec = Gem::Specification.from_yaml entry.read
@spec = Gem::Specification.from_yaml limit_read(entry, "metadata", limit)
when "metadata.gz" then
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
Expand All @@ -557,7 +558,7 @@ def read_checksums(gem)

@checksums = gem.seek "checksums.yaml.gz" do |entry|
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
Expand Down Expand Up @@ -664,7 +665,7 @@ def verify_entry(entry)

case file_name
when /\.sig$/ then
@signatures[$`] = entry.read if @security_policy
@signatures[$`] = limit_read(entry, file_name, 1024 * 1024) if @security_policy
return
else
digest entry
Expand Down Expand Up @@ -715,7 +716,7 @@ def verify_gz(entry) # :nodoc:
raise Gem::Package::FormatError.new(e.message, entry.full_name)
end

if RUBY_ENGINE == "truffleruby"
if RUBY_ENGINE == "truffleruby" && RUBY_ENGINE_VERSION < "23.1.2"
def copy_stream(src, dst) # :nodoc:
dst.write src.read
end
Expand All @@ -724,6 +725,12 @@ def copy_stream(src, dst) # :nodoc:
IO.copy_stream(src, dst)
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

require_relative "package/digest_io"
Expand Down

0 comments on commit a596e3c

Please sign in to comment.