rubyzip 3.3.1 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05f84643ff59429233f25e059dc64a55006ef51a7ff77163993fa03fa5bd0fb3
4
- data.tar.gz: 182f12bf76a7bf64bb589d002195bb64a00ef0309aaa415670182c59db05b8f5
3
+ metadata.gz: 31692c3a3abbfc979783a52acfad553e88440ef25ef3375e2e7f2355407999ef
4
+ data.tar.gz: 16f79be412b8048c8db41fb4a9807b6743adf390f80d2ee73580eb98cf5c7073
5
5
  SHA512:
6
- metadata.gz: b6bbacb2a7f5be8886a00f6a0a29bf8777ab82f1002912bcbe9b80e5405ab54d6cf6434d26c6472a041542f90938317d62249d0a9a9b151d908f8c45d421a298
7
- data.tar.gz: 90575f372b006b74d1f437b9950b48888a011f89d8110e231a225952d558e88fe4f5f1a84953f95b8caf6009c83325b4d0e8c40cdd8fc4c36b1df1b9dd1af14f
6
+ metadata.gz: 614763745e03176e441f401d739c79bd4df37483d7d3d9fd0821a8119906048f2b3ebbbd9604653e273e7c115e41176b1d3a3ea3f5199d6701cda2f9b30bb777
7
+ data.tar.gz: e704da45a03de72a67e108a037aac218bb397f1a625f50ca8ea6dbce842bb1a312de6679662bc4154afbb9229f4f5136f3aac981d651615dbde6c7b9a66dade7
data/Changelog.md CHANGED
@@ -1,3 +1,20 @@
1
+ # 3.4.0 (2026-06-14)
2
+
3
+ - Prevent entries from being extracted outside specified directory. [#664](https://github.com/rubyzip/rubyzip/issues/664). Thanks to @connorshea for additional reporting on this.
4
+ - Use `SecureRandom` in place of insecure `Random`.
5
+ - Stop reading the central directory on first error.
6
+ - Add a check on number of declared entries in a zip file. Thanks to @connorshea for reporting this.
7
+ - Add note to README re reporting security issues privately.
8
+ - Add lib/rubyzip.rb for Bundler auto-require. [#660](https://github.com/rubyzip/rubyzip/pull/660)
9
+
10
+ Tooling/internal:
11
+
12
+ - Replace the test Excel spreadsheet fixture.
13
+ - Use `assert_silent` shorthand when expecting no output from a test.
14
+ - Clean up CentralDirectory instance variables.
15
+ - Add Ruby 4.0 to the Windows CI and update CI matrix in the README.
16
+ - List ZIP docs that we store here and link to online versions. [#657](https://github.com/rubyzip/rubyzip/issues/657)
17
+
1
18
  # 3.3.1 (2026-05-30)
2
19
 
3
20
  - Reinstate default param for `InputStream#sysread`. [#663](https://github.com/rubyzip/rubyzip/issues/663)
data/README.md CHANGED
@@ -11,6 +11,10 @@ Rubyzip is a ruby library for reading and writing zip files.
11
11
 
12
12
  ## Important notes
13
13
 
14
+ ### Reporting security issues with this library
15
+
16
+ If you think you have found a security issue with this library, please don't submit a public issue or PR. Email me directly at hainesr@gmail.com with as much information as you can provide - steps for replication are particularly helpful if you can - and we'll get it sorted ASAP. Thank you.
17
+
14
18
  ### Updating to version 3.0
15
19
 
16
20
  The public API of some classes has been modernized to use named parameters for optional arguments. Please check your usage of the following Rubyzip classes:
@@ -40,9 +44,12 @@ gem install rubyzip
40
44
  Or in your Gemfile:
41
45
 
42
46
  ```ruby
43
- gem 'rubyzip'
47
+ gem 'rubyzip', require: 'zip' # For versions before 3.4.
48
+ gem 'rubyzip' # For version 3.4 and after.
44
49
  ```
45
50
 
51
+ From version 3.4 onwards, you can `require` either 'zip' or 'rubyzip' to use this library. Before version 3.4 you need to `require 'zip'` explicitly.
52
+
46
53
  ## Usage
47
54
 
48
55
  ### Basic zip archive creation
@@ -363,7 +370,19 @@ Some zip files might have an invalid date format, which will raise a warning. Yo
363
370
  Zip.warn_invalid_date = false
364
371
  ```
365
372
 
366
- ### Size Validation
373
+ ### Validating Declared Number of Entries
374
+
375
+ When reading a zip file it is potentially dangerous to trust what it tells you about how many entries it contains. A malformed zip file could claim a high number of entries in an attempt to waste internal resources, or crash the processing application.
376
+
377
+ By default rubyzip will warn if the number of declared entries is impossible based on the actual size of the Central Directory headers. You can set this check to raise an error:
378
+
379
+ ```ruby
380
+ Zip.validate_declared_number_of_entries = true
381
+ ```
382
+
383
+ It is likely that the default behaviour for this check will be changed to raise an error in version 4.
384
+
385
+ ### Entry Size Validation
367
386
 
368
387
  By default (in rubyzip >= 2.0), rubyzip's `extract` method checks that an entry's reported uncompressed size is not (significantly) smaller than its actual size. This is to help you protect your application against [zip bombs](https://en.wikipedia.org/wiki/Zip_bomb). Before `extract`ing an entry, you should check that its size is in the range you expect. For example, if your application supports processing up to 100 files at once, each up to 10MiB, your zip extraction code might look like:
369
388
 
@@ -445,11 +464,12 @@ Rubyzip 2.4 is known to work on MRI 2.4 to 3.4 on Linux and Mac, and JRuby and T
445
464
 
446
465
  Please see the table below for what we think the current situation is. Note: an empty cell means "unknown", not "does not work".
447
466
 
448
- | OS/Ruby | 3.0 | 3.1 | 3.2 | 3.3 | 3.4 | Head | JRuby 10.0.1.0 | JRuby Head | Truffleruby 24.2.1 | Truffleruby Head |
449
- |---------|-----|-----|-----|-----|-----|------|---------------|------------|--------------------|------------------|
450
- |Ubuntu 24.04| CI | CI | CI | CI | CI | ci | CI | ci | CI | ci |
451
- |Mac OS 14.7.6| CI | CI | CI | CI | CI | ci | x | | x | |
452
- |Windows Server 2022| CI | | | | CI&nbsp;mswin</br>CI&nbsp;ucrt | | | | | |
467
+ | OS/Ruby | 3.0 | 3.1 | 3.2 | 3.3 | 3.4 | 4.0 | Head | JRuby 10.0.1.0 | JRuby Head | Truffleruby 34.0.1 | Truffleruby Head |
468
+ |---------|-----|-----|-----|-----|-----|-----|------|----------------|------------|-------------------|------------------|
469
+ |Ubuntu 24.04| CI | CI | CI | CI | CI | CI | ci | CI | ci | CI | ci |
470
+ |Mac OS 15.7.7| CI | x | x | CI | CI | CI | ci | x | | x | |
471
+ |Windows Server 2022| CI | | | | | | | | | | |
472
+ |Windows Server 2025| | | | CI | | CI | CI&nbsp;mswin</br>CI&nbsp;ucrt | | | | |
453
473
 
454
474
  Key: `CI` - tested in CI, should work; `ci` - tested in CI, might fail; `x` - known working; `o` - known failing.
455
475
 
data/lib/rubyzip.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'zip'
@@ -114,18 +114,20 @@ module Zip
114
114
 
115
115
  def unpack_64_e_o_c_d(buffer) # :nodoc:
116
116
  _, # ZIP64_END_OF_CD_SIG. We know we have this at this point.
117
- @size_of_zip64_e_o_c_d,
118
- @version_made_by,
119
- @version_needed_for_extract,
120
- @number_of_this_disk,
121
- @number_of_disk_with_start_of_cdir,
122
- @total_number_of_entries_in_cdir_on_this_disk,
117
+ size_of_zip64_e_o_c_d,
118
+ _version_made_by,
119
+ _version_needed_for_extract,
120
+ _number_of_this_disk,
121
+ _number_of_disk_with_start_of_cdir,
122
+ _total_number_of_entries_in_cdir_on_this_disk,
123
123
  @size,
124
- @size_in_bytes,
124
+ size_in_bytes,
125
125
  @cdir_offset = buffer.unpack('VQ<vvVVQ<Q<Q<Q<')
126
126
 
127
+ validate_size!(@size, size_in_bytes)
128
+
127
129
  zip64_extensible_data_size =
128
- @size_of_zip64_e_o_c_d - ZIP64_STATIC_EOCD_SIZE + 12
130
+ size_of_zip64_e_o_c_d - ZIP64_STATIC_EOCD_SIZE + 12
129
131
  @zip64_extensible_data = if zip64_extensible_data_size.zero?
130
132
  ''
131
133
  else
@@ -145,25 +147,29 @@ module Zip
145
147
 
146
148
  # Unpack the EOCD and return a boolean indicating whether this header is
147
149
  # complete without needing Zip64 extensions.
148
- def unpack_e_o_c_d(buffer) # :nodoc: # rubocop:disable Naming/PredicateMethod
150
+ def unpack_e_o_c_d(buffer) # :nodoc:
149
151
  _, # END_OF_CD_SIG. We know we have this at this point.
150
- @number_of_this_disk,
151
- @number_of_disk_with_start_of_cdir,
152
- @total_number_of_entries_in_cdir_on_this_disk,
152
+ number_of_this_disk,
153
+ number_of_disk_with_start_of_cdir,
154
+ total_number_of_entries_in_cdir_on_this_disk,
153
155
  @size,
154
- @size_in_bytes,
156
+ size_in_bytes,
155
157
  @cdir_offset,
156
158
  comment_length = buffer.unpack('VvvvvVVv')
157
159
 
160
+ complete = !([number_of_this_disk, number_of_disk_with_start_of_cdir,
161
+ total_number_of_entries_in_cdir_on_this_disk, @size].any?(0xFFFF) ||
162
+ size_in_bytes == 0xFFFFFFFF || @cdir_offset == 0xFFFFFFFF)
163
+
164
+ validate_size!(@size, size_in_bytes) if complete
165
+
158
166
  @comment = if comment_length.positive?
159
167
  buffer.slice(STATIC_EOCD_SIZE, comment_length)
160
168
  else
161
169
  ''
162
170
  end
163
171
 
164
- !([@number_of_this_disk, @number_of_disk_with_start_of_cdir,
165
- @total_number_of_entries_in_cdir_on_this_disk, @size].any?(0xFFFF) ||
166
- @size_in_bytes == 0xFFFFFFFF || @cdir_offset == 0xFFFFFFFF)
172
+ complete
167
173
  end
168
174
 
169
175
  def read_central_directory_entries(io) # :nodoc:
@@ -180,7 +186,7 @@ module Zip
180
186
  @entry_set = EntrySet.new
181
187
  @size.times do
182
188
  entry = Entry.read_c_dir_entry(io)
183
- next unless entry
189
+ break unless entry
184
190
 
185
191
  offset = if entry.zip64?
186
192
  entry.extra[:zip64].relative_header_offset
@@ -258,6 +264,15 @@ module Zip
258
264
 
259
265
  [io.tell, io.read]
260
266
  end
267
+
268
+ def validate_size!(size, size_in_bytes)
269
+ return if size * CDIR_ENTRY_STATIC_HEADER_LENGTH <= size_in_bytes
270
+
271
+ error = EntryNumberMismatchError.new(size, size_in_bytes)
272
+ raise error if Zip.validate_declared_number_of_entries
273
+
274
+ warn error.message
275
+ end
261
276
  end
262
277
  end
263
278
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'securerandom'
4
+
3
5
  module Zip
4
6
  module TraditionalEncryption # :nodoc:
5
7
  def initialize(password)
@@ -44,7 +46,7 @@ module Zip
44
46
  def header(mtime)
45
47
  [].tap do |header|
46
48
  (header_bytesize - 2).times do
47
- header << Random.rand(0..255)
49
+ header << SecureRandom.rand(0..255)
48
50
  end
49
51
  header << (mtime.to_binary_dos_time & 0xff)
50
52
  header << (mtime.to_binary_dos_time >> 8)
data/lib/zip/entry.rb CHANGED
@@ -290,7 +290,7 @@ module Zip
290
290
  dest_dir = ::File.absolute_path(destination_directory || '.')
291
291
  extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))
292
292
 
293
- unless extract_path.start_with?(dest_dir)
293
+ unless extract_path.start_with?(dest_dir + ::File::SEPARATOR) || dest_dir == ::File::SEPARATOR
294
294
  warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
295
295
  return self
296
296
  end
data/lib/zip/errors.rb CHANGED
@@ -109,6 +109,22 @@ module Zip
109
109
  end
110
110
  end
111
111
 
112
+ class EntryNumberMismatchError < Error
113
+ # Create a new EntryNumberMismatchError with the specified size and size in bytes.
114
+ def initialize(size, size_in_bytes)
115
+ super()
116
+ @size = size
117
+ @size_in_bytes = size_in_bytes
118
+ end
119
+
120
+ # The message returned by this error.
121
+ def message
122
+ "Zip consistency problem: an impossibly high number of entries (#{@size}) " \
123
+ 'is declared in this file, compared to the size of the central directory ' \
124
+ "(#{@size_in_bytes} bytes)."
125
+ end
126
+ end
127
+
112
128
  # Error raised if a split archive is read. Rubyzip does not support reading
113
129
  # split archives.
114
130
  class SplitArchiveError < Error
data/lib/zip/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Zip
4
4
  # The version of the Rubyzip library.
5
- VERSION = '3.3.1'
5
+ VERSION = '3.4.0'
6
6
  end
data/lib/zip.rb CHANGED
@@ -54,7 +54,8 @@ module Zip
54
54
  :warn_invalid_date,
55
55
  :case_insensitive_match,
56
56
  :force_entry_names_encoding,
57
- :validate_entry_sizes
57
+ :validate_entry_sizes,
58
+ :validate_declared_number_of_entries
58
59
 
59
60
  DEFAULT_RESTORE_OPTIONS = {
60
61
  restore_ownership: false,
@@ -78,6 +79,7 @@ module Zip
78
79
  @case_insensitive_match = false
79
80
  @force_entry_names_encoding = nil
80
81
  @validate_entry_sizes = true
82
+ @validate_declared_number_of_entries = false # Set this to `true` in v4.0.0?
81
83
  end
82
84
 
83
85
  # Set options for RubyZip in one block.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyzip
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.1
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Haines
@@ -135,6 +135,7 @@ files:
135
135
  - LICENSE.md
136
136
  - README.md
137
137
  - Rakefile
138
+ - lib/rubyzip.rb
138
139
  - lib/zip.rb
139
140
  - lib/zip/central_directory.rb
140
141
  - lib/zip/compressor.rb
@@ -195,9 +196,9 @@ licenses:
195
196
  - BSD-2-Clause
196
197
  metadata:
197
198
  bug_tracker_uri: https://github.com/rubyzip/rubyzip/issues
198
- changelog_uri: https://github.com/rubyzip/rubyzip/blob/v3.3.1/Changelog.md
199
- documentation_uri: https://www.rubydoc.info/gems/rubyzip/3.3.1
200
- source_code_uri: https://github.com/rubyzip/rubyzip/tree/v3.3.1
199
+ changelog_uri: https://github.com/rubyzip/rubyzip/blob/v3.4.0/Changelog.md
200
+ documentation_uri: https://www.rubydoc.info/gems/rubyzip/3.4.0
201
+ source_code_uri: https://github.com/rubyzip/rubyzip/tree/v3.4.0
201
202
  wiki_uri: https://github.com/rubyzip/rubyzip/wiki
202
203
  rubygems_mfa_required: 'true'
203
204
  rdoc_options: []