rubyzip 1.2.1 → 1.2.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rubyzip might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/README.md +24 -17
- data/lib/zip.rb +9 -1
- data/lib/zip/central_directory.rb +3 -3
- data/lib/zip/compressor.rb +1 -2
- data/lib/zip/constants.rb +3 -3
- data/lib/zip/crypto/null_encryption.rb +2 -4
- data/lib/zip/decompressor.rb +1 -1
- data/lib/zip/dos_time.rb +1 -1
- data/lib/zip/entry.rb +46 -49
- data/lib/zip/entry_set.rb +3 -3
- data/lib/zip/extra_field.rb +1 -1
- data/lib/zip/extra_field/generic.rb +1 -1
- data/lib/zip/extra_field/zip64_placeholder.rb +1 -2
- data/lib/zip/file.rb +12 -12
- data/lib/zip/filesystem.rb +17 -13
- data/lib/zip/inflater.rb +1 -1
- data/lib/zip/input_stream.rb +10 -7
- data/lib/zip/ioextras/abstract_input_stream.rb +1 -1
- data/lib/zip/ioextras/abstract_output_stream.rb +1 -1
- data/lib/zip/output_stream.rb +5 -5
- data/lib/zip/pass_thru_decompressor.rb +1 -1
- data/lib/zip/streamable_stream.rb +1 -1
- data/lib/zip/version.rb +1 -1
- data/samples/example_recursive.rb +14 -15
- data/samples/gtk_ruby_zip.rb +1 -1
- data/samples/qtzip.rb +1 -1
- data/samples/zipfind.rb +2 -2
- data/test/central_directory_entry_test.rb +1 -1
- data/test/data/gpbit3stored.zip +0 -0
- data/test/data/path_traversal/Makefile +10 -0
- data/test/data/path_traversal/jwilk/README.md +5 -0
- data/test/data/path_traversal/jwilk/absolute1.zip +0 -0
- data/test/data/path_traversal/jwilk/absolute2.zip +0 -0
- data/test/data/path_traversal/jwilk/dirsymlink.zip +0 -0
- data/test/data/path_traversal/jwilk/dirsymlink2a.zip +0 -0
- data/test/data/path_traversal/jwilk/dirsymlink2b.zip +0 -0
- data/test/data/path_traversal/jwilk/relative0.zip +0 -0
- data/test/data/path_traversal/jwilk/relative2.zip +0 -0
- data/test/data/path_traversal/jwilk/symlink.zip +0 -0
- data/test/data/path_traversal/relative1.zip +0 -0
- data/test/data/path_traversal/tuzovakaoff/README.md +3 -0
- data/test/data/path_traversal/tuzovakaoff/absolutepath.zip +0 -0
- data/test/data/path_traversal/tuzovakaoff/symlink.zip +0 -0
- data/test/data/rubycode.zip +0 -0
- data/test/errors_test.rb +1 -0
- data/test/file_permissions_test.rb +11 -15
- data/test/file_test.rb +27 -9
- data/test/filesystem/dir_iterator_test.rb +1 -1
- data/test/filesystem/directory_test.rb +29 -11
- data/test/filesystem/file_mutating_test.rb +3 -4
- data/test/filesystem/file_nonmutating_test.rb +31 -31
- data/test/filesystem/file_stat_test.rb +4 -4
- data/test/gentestfiles.rb +13 -13
- data/test/input_stream_test.rb +6 -6
- data/test/ioextras/abstract_output_stream_test.rb +2 -2
- data/test/path_traversal_test.rb +134 -0
- data/test/test_helper.rb +2 -2
- data/test/unicode_file_names_and_comments_test.rb +12 -0
- data/test/zip64_full_test.rb +2 -2
- metadata +95 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 898367588fc593bddd565ee8626c75cfbcddfce2
|
4
|
+
data.tar.gz: 24d90fd344a11d8cf3b8098c459eb2c765e933e7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f00c8c74720ba3bf103596601400f57e89d03cdd90b4423debd4c96ed6150a3db0fa8f7407201657cf6a7ee0b2e6586c7a284e1398dd1cf44c523799e1b25be
|
7
|
+
data.tar.gz: 3a434114b84d7b109e26aec4821b395ad100efda591ad004656c6b5fd8cdba3740e50c171409c726a9770996e0815ef0324c87063d975024642c6f89ba56377e
|
data/README.md
CHANGED
@@ -52,9 +52,9 @@ Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile|
|
|
52
52
|
# Two arguments:
|
53
53
|
# - The name of the file as it will appear in the archive
|
54
54
|
# - The original file, including the path to find it
|
55
|
-
zipfile.add(filename, folder
|
55
|
+
zipfile.add(filename, File.join(folder, filename))
|
56
56
|
end
|
57
|
-
zipfile.get_output_stream("myFile") { |
|
57
|
+
zipfile.get_output_stream("myFile") { |f| f.write "myFile contains just this" }
|
58
58
|
end
|
59
59
|
```
|
60
60
|
|
@@ -85,36 +85,36 @@ class ZipFileGenerator
|
|
85
85
|
def write
|
86
86
|
entries = Dir.entries(@input_dir) - %w(. ..)
|
87
87
|
|
88
|
-
::Zip::File.open(@output_file, ::Zip::File::CREATE) do |
|
89
|
-
write_entries entries, '',
|
88
|
+
::Zip::File.open(@output_file, ::Zip::File::CREATE) do |zipfile|
|
89
|
+
write_entries entries, '', zipfile
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
93
93
|
private
|
94
94
|
|
95
95
|
# A helper method to make the recursion work.
|
96
|
-
def write_entries(entries, path,
|
96
|
+
def write_entries(entries, path, zipfile)
|
97
97
|
entries.each do |e|
|
98
|
-
|
99
|
-
disk_file_path = File.join(@input_dir,
|
98
|
+
zipfile_path = path == '' ? e : File.join(path, e)
|
99
|
+
disk_file_path = File.join(@input_dir, zipfile_path)
|
100
100
|
puts "Deflating #{disk_file_path}"
|
101
101
|
|
102
102
|
if File.directory? disk_file_path
|
103
|
-
recursively_deflate_directory(disk_file_path,
|
103
|
+
recursively_deflate_directory(disk_file_path, zipfile, zipfile_path)
|
104
104
|
else
|
105
|
-
put_into_archive(disk_file_path,
|
105
|
+
put_into_archive(disk_file_path, zipfile, zipfile_path)
|
106
106
|
end
|
107
107
|
end
|
108
108
|
end
|
109
109
|
|
110
|
-
def recursively_deflate_directory(disk_file_path,
|
111
|
-
|
110
|
+
def recursively_deflate_directory(disk_file_path, zipfile, zipfile_path)
|
111
|
+
zipfile.mkdir zipfile_path
|
112
112
|
subdir = Dir.entries(disk_file_path) - %w(. ..)
|
113
|
-
write_entries subdir,
|
113
|
+
write_entries subdir, zipfile_path, zipfile
|
114
114
|
end
|
115
115
|
|
116
|
-
def put_into_archive(disk_file_path,
|
117
|
-
|
116
|
+
def put_into_archive(disk_file_path, zipfile, zipfile_path)
|
117
|
+
zipfile.get_output_stream(zipfile_path) do |f|
|
118
118
|
f.write(File.open(disk_file_path, 'rb').read)
|
119
119
|
end
|
120
120
|
end
|
@@ -175,9 +175,8 @@ end
|
|
175
175
|
|
176
176
|
But there is one exception when it is not working - General Purpose Flag Bit 3.
|
177
177
|
|
178
|
-
|
179
|
-
|
180
|
-
```
|
178
|
+
> If bit 3 (0x08) of the general-purpose flags field is set, then the CRC-32 and file sizes are not known when the header is written. The fields in the local header are filled with zero, and the CRC-32 and size are appended in a 12-byte structure (optionally preceded by a 4-byte signature) immediately after the compressed data
|
179
|
+
|
181
180
|
|
182
181
|
If `::Zip::InputStream` finds such entry in the zip archive it will raise an exception.
|
183
182
|
|
@@ -254,6 +253,14 @@ Zip.default_compression = Zlib::DEFAULT_COMPRESSION
|
|
254
253
|
```
|
255
254
|
It defaults to `Zlib::DEFAULT_COMPRESSION`. Possible values are `Zlib::BEST_COMPRESSION`, `Zlib::DEFAULT_COMPRESSION` and `Zlib::NO_COMPRESSION`
|
256
255
|
|
256
|
+
Sometimes file names inside zip contain non-ASCII characters. If you can assume which encoding was used for such names and want to be able to find such entries using `find_entry` then you can force assumed encoding like so:
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
Zip.force_entry_names_encoding = 'UTF-8'
|
260
|
+
```
|
261
|
+
|
262
|
+
Allowed encoding names are the same as accepted by `String#force_encoding`
|
263
|
+
|
257
264
|
You can set multiple settings at the same time by using a block:
|
258
265
|
|
259
266
|
```ruby
|
data/lib/zip.rb
CHANGED
@@ -34,7 +34,15 @@ require 'zip/errors'
|
|
34
34
|
|
35
35
|
module Zip
|
36
36
|
extend self
|
37
|
-
attr_accessor :unicode_names,
|
37
|
+
attr_accessor :unicode_names,
|
38
|
+
:on_exists_proc,
|
39
|
+
:continue_on_exists_proc,
|
40
|
+
:sort_entries,
|
41
|
+
:default_compression,
|
42
|
+
:write_zip64_support,
|
43
|
+
:warn_invalid_date,
|
44
|
+
:case_insensitive_match,
|
45
|
+
:force_entry_names_encoding
|
38
46
|
|
39
47
|
def reset!
|
40
48
|
@_ran_once = false
|
@@ -96,7 +96,7 @@ module Zip
|
|
96
96
|
@size_in_bytes = Entry.read_zip_64_long(buf)
|
97
97
|
@cdir_offset = Entry.read_zip_64_long(buf)
|
98
98
|
@zip_64_extensible = buf.slice!(0, buf.bytesize)
|
99
|
-
raise Error, 'Zip consistency problem while reading eocd structure' unless buf.
|
99
|
+
raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty?
|
100
100
|
end
|
101
101
|
|
102
102
|
def read_e_o_c_d(buf) #:nodoc:
|
@@ -113,7 +113,7 @@ module Zip
|
|
113
113
|
else
|
114
114
|
buf.read(comment_length)
|
115
115
|
end
|
116
|
-
raise Error, 'Zip consistency problem while reading eocd structure' unless buf.
|
116
|
+
raise Error, 'Zip consistency problem while reading eocd structure' unless buf.empty?
|
117
117
|
end
|
118
118
|
|
119
119
|
def read_central_directory_entries(io) #:nodoc:
|
@@ -130,7 +130,7 @@ module Zip
|
|
130
130
|
|
131
131
|
def read_from_stream(io) #:nodoc:
|
132
132
|
buf = start_buf(io)
|
133
|
-
if
|
133
|
+
if zip64_file?(buf)
|
134
134
|
read_64_e_o_c_d(buf)
|
135
135
|
else
|
136
136
|
read_e_o_c_d(buf)
|
data/lib/zip/compressor.rb
CHANGED
data/lib/zip/constants.rb
CHANGED
@@ -11,9 +11,9 @@ module Zip
|
|
11
11
|
VERSION_NEEDED_TO_EXTRACT = 20
|
12
12
|
VERSION_NEEDED_TO_EXTRACT_ZIP64 = 45
|
13
13
|
|
14
|
-
FILE_TYPE_FILE =
|
15
|
-
FILE_TYPE_DIR =
|
16
|
-
FILE_TYPE_SYMLINK =
|
14
|
+
FILE_TYPE_FILE = 0o10
|
15
|
+
FILE_TYPE_DIR = 0o04
|
16
|
+
FILE_TYPE_SYMLINK = 0o12
|
17
17
|
|
18
18
|
FSTYPE_FAT = 0
|
19
19
|
FSTYPE_AMIGA = 1
|
data/lib/zip/decompressor.rb
CHANGED
data/lib/zip/dos_time.rb
CHANGED
data/lib/zip/entry.rb
CHANGED
@@ -99,7 +99,7 @@ module Zip
|
|
99
99
|
end
|
100
100
|
|
101
101
|
# Dynamic checkers
|
102
|
-
%w
|
102
|
+
%w[directory file symlink].each do |k|
|
103
103
|
define_method "#{k}?" do
|
104
104
|
file_type_is?(k.to_sym)
|
105
105
|
end
|
@@ -109,6 +109,17 @@ module Zip
|
|
109
109
|
@name.end_with?('/')
|
110
110
|
end
|
111
111
|
|
112
|
+
# Is the name a relative path, free of `..` patterns that could lead to
|
113
|
+
# path traversal attacks? This does NOT handle symlinks; if the path
|
114
|
+
# contains symlinks, this check is NOT enough to guarantee safety.
|
115
|
+
def name_safe?
|
116
|
+
cleanpath = Pathname.new(@name).cleanpath
|
117
|
+
return false unless cleanpath.relative?
|
118
|
+
root = ::File::SEPARATOR
|
119
|
+
naive_expanded_path = ::File.join(root, cleanpath.to_s)
|
120
|
+
cleanpath.expand_path(root).to_s == naive_expanded_path
|
121
|
+
end
|
122
|
+
|
112
123
|
def local_entry_offset #:nodoc:all
|
113
124
|
local_header_offset + @local_header_size
|
114
125
|
end
|
@@ -147,14 +158,17 @@ module Zip
|
|
147
158
|
end
|
148
159
|
|
149
160
|
# Extracts entry to file dest_path (defaults to @name).
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
if
|
154
|
-
puts "WARNING: skipped
|
161
|
+
# NB: The caller is responsible for making sure dest_path is safe, if it
|
162
|
+
# is passed.
|
163
|
+
def extract(dest_path = nil, &block)
|
164
|
+
if dest_path.nil? && !name_safe?
|
165
|
+
puts "WARNING: skipped #{@name} as unsafe"
|
155
166
|
return self
|
156
167
|
end
|
157
168
|
|
169
|
+
dest_path ||= @name
|
170
|
+
block ||= proc { ::Zip.on_exists_proc }
|
171
|
+
|
158
172
|
if directory? || file? || symlink?
|
159
173
|
__send__("create_#{@ftype}", dest_path, &block)
|
160
174
|
else
|
@@ -239,7 +253,10 @@ module Zip
|
|
239
253
|
@name = io.read(@name_length)
|
240
254
|
extra = io.read(@extra_length)
|
241
255
|
|
242
|
-
@name.
|
256
|
+
@name.tr!('\\', '/')
|
257
|
+
if ::Zip.force_entry_names_encoding
|
258
|
+
@name.force_encoding(::Zip.force_entry_names_encoding)
|
259
|
+
end
|
243
260
|
|
244
261
|
if extra && extra.bytesize != @extra_length
|
245
262
|
raise ::Zip::Error, 'Truncated local zip entry header'
|
@@ -263,8 +280,8 @@ module Zip
|
|
263
280
|
@time.to_binary_dos_time, # @last_mod_time ,
|
264
281
|
@time.to_binary_dos_date, # @last_mod_date ,
|
265
282
|
@crc,
|
266
|
-
|
267
|
-
|
283
|
+
zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
|
284
|
+
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
|
268
285
|
name_size,
|
269
286
|
@extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
|
270
287
|
end
|
@@ -308,7 +325,7 @@ module Zip
|
|
308
325
|
def set_ftype_from_c_dir_entry
|
309
326
|
@ftype = case @fstype
|
310
327
|
when ::Zip::FSTYPE_UNIX
|
311
|
-
@unix_perms = (@external_file_attributes >> 16) &
|
328
|
+
@unix_perms = (@external_file_attributes >> 16) & 0o7777
|
312
329
|
case (@external_file_attributes >> 28)
|
313
330
|
when ::Zip::FILE_TYPE_DIR
|
314
331
|
:directory
|
@@ -364,6 +381,9 @@ module Zip
|
|
364
381
|
check_c_dir_entry_signature
|
365
382
|
set_time(@last_mod_date, @last_mod_time)
|
366
383
|
@name = io.read(@name_length)
|
384
|
+
if ::Zip.force_entry_names_encoding
|
385
|
+
@name.force_encoding(::Zip.force_entry_names_encoding)
|
386
|
+
end
|
367
387
|
read_c_dir_extra_field(io)
|
368
388
|
@comment = io.read(@comment_length)
|
369
389
|
check_c_dir_entry_comment_size
|
@@ -384,14 +404,14 @@ module Zip
|
|
384
404
|
stat = file_stat(path)
|
385
405
|
@unix_uid = stat.uid
|
386
406
|
@unix_gid = stat.gid
|
387
|
-
@unix_perms = stat.mode &
|
407
|
+
@unix_perms = stat.mode & 0o7777
|
388
408
|
end
|
389
409
|
|
390
410
|
def set_unix_permissions_on_path(dest_path)
|
391
411
|
# BUG: does not update timestamps into account
|
392
412
|
# ignore setuid/setgid bits by default. honor if @restore_ownership
|
393
|
-
unix_perms_mask =
|
394
|
-
unix_perms_mask =
|
413
|
+
unix_perms_mask = 0o1777
|
414
|
+
unix_perms_mask = 0o7777 if @restore_ownership
|
395
415
|
::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path) if @restore_permissions && @unix_perms
|
396
416
|
::FileUtils.chown(@unix_uid, @unix_gid, dest_path) if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
|
397
417
|
# File::utimes()
|
@@ -418,15 +438,15 @@ module Zip
|
|
418
438
|
@time.to_binary_dos_time, # @last_mod_time ,
|
419
439
|
@time.to_binary_dos_date, # @last_mod_date ,
|
420
440
|
@crc,
|
421
|
-
|
422
|
-
|
441
|
+
zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
|
442
|
+
zip64 && zip64.original_size ? 0xFFFFFFFF : @size,
|
423
443
|
name_size,
|
424
444
|
@extra ? @extra.c_dir_size : 0,
|
425
445
|
comment_size,
|
426
|
-
|
446
|
+
zip64 && zip64.disk_start_number ? 0xFFFF : 0, # disk number start
|
427
447
|
@internal_file_attributes, # file type (binary=0, text=1)
|
428
448
|
@external_file_attributes, # native filesystem attributes
|
429
|
-
|
449
|
+
zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset,
|
430
450
|
@name,
|
431
451
|
@extra,
|
432
452
|
@comment
|
@@ -439,18 +459,18 @@ module Zip
|
|
439
459
|
when ::Zip::FSTYPE_UNIX
|
440
460
|
ft = case @ftype
|
441
461
|
when :file
|
442
|
-
@unix_perms ||=
|
462
|
+
@unix_perms ||= 0o644
|
443
463
|
::Zip::FILE_TYPE_FILE
|
444
464
|
when :directory
|
445
|
-
@unix_perms ||=
|
465
|
+
@unix_perms ||= 0o755
|
446
466
|
::Zip::FILE_TYPE_DIR
|
447
467
|
when :symlink
|
448
|
-
@unix_perms ||=
|
468
|
+
@unix_perms ||= 0o755
|
449
469
|
::Zip::FILE_TYPE_SYMLINK
|
450
470
|
end
|
451
471
|
|
452
472
|
unless ft.nil?
|
453
|
-
@external_file_attributes = (ft << 12 | (@unix_perms &
|
473
|
+
@external_file_attributes = (ft << 12 | (@unix_perms & 0o7777)) << 16
|
454
474
|
end
|
455
475
|
end
|
456
476
|
|
@@ -464,7 +484,7 @@ module Zip
|
|
464
484
|
def ==(other)
|
465
485
|
return false unless other.class == self.class
|
466
486
|
# Compares contents of local entry and exposed fields
|
467
|
-
keys_equal = %w
|
487
|
+
keys_equal = %w[compression_method crc compressed_size size name extra filepath].all? do |k|
|
468
488
|
other.__send__(k.to_sym) == __send__(k.to_sym)
|
469
489
|
end
|
470
490
|
keys_equal && time.dos_equals(other.time)
|
@@ -494,7 +514,7 @@ module Zip
|
|
494
514
|
end
|
495
515
|
else
|
496
516
|
zis = ::Zip::InputStream.new(@zipfile, local_header_offset)
|
497
|
-
zis.instance_variable_set(:@
|
517
|
+
zis.instance_variable_set(:@complete_entry, self)
|
498
518
|
zis.get_next_entry
|
499
519
|
if block_given?
|
500
520
|
begin
|
@@ -607,32 +627,9 @@ module Zip
|
|
607
627
|
|
608
628
|
# BUG: create_symlink() does not use &block
|
609
629
|
def create_symlink(dest_path)
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
rescue Errno::ENOENT
|
614
|
-
end
|
615
|
-
|
616
|
-
io = get_input_stream
|
617
|
-
linkto = io.read
|
618
|
-
|
619
|
-
if stat
|
620
|
-
if stat.symlink?
|
621
|
-
if ::File.readlink(dest_path) == linkto
|
622
|
-
return
|
623
|
-
else
|
624
|
-
raise ::Zip::DestinationFileExistsError,
|
625
|
-
"Cannot create symlink '#{dest_path}'. " \
|
626
|
-
'A symlink already exists with that name'
|
627
|
-
end
|
628
|
-
else
|
629
|
-
raise ::Zip::DestinationFileExistsError,
|
630
|
-
"Cannot create symlink '#{dest_path}'. " \
|
631
|
-
'A file already exists with that name'
|
632
|
-
end
|
633
|
-
end
|
634
|
-
|
635
|
-
::File.symlink(linkto, dest_path)
|
630
|
+
# TODO: Symlinks pose security challenges. Symlink support temporarily
|
631
|
+
# removed in view of https://github.com/rubyzip/rubyzip/issues/369 .
|
632
|
+
puts "WARNING: skipped symlink #{dest_path}"
|
636
633
|
end
|
637
634
|
|
638
635
|
# apply missing data from the zip64 extra information field, if present
|
data/lib/zip/entry_set.rb
CHANGED
@@ -5,7 +5,7 @@ module Zip
|
|
5
5
|
|
6
6
|
def initialize(an_enumerable = [])
|
7
7
|
super()
|
8
|
-
@entry_set
|
8
|
+
@entry_set = {}
|
9
9
|
an_enumerable.each { |o| push(o) }
|
10
10
|
end
|
11
11
|
|
@@ -33,9 +33,9 @@ module Zip
|
|
33
33
|
entry if @entry_set.delete(to_key(entry))
|
34
34
|
end
|
35
35
|
|
36
|
-
def each
|
36
|
+
def each
|
37
37
|
@entry_set = sorted_entries.dup.each do |_, value|
|
38
|
-
|
38
|
+
yield(value)
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
data/lib/zip/extra_field.rb
CHANGED