diff --git a/.gitignore b/.gitignore index b66ef97..c065e9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.md *.swp *~ .rake_tasks~ @@ -10,7 +11,6 @@ doc html pkg publish -ri test/cache.tst tmp/ .byebug_history diff --git a/.hoerc b/.hoerc index cd32cc1..bc8cb68 100644 --- a/.hoerc +++ b/.hoerc @@ -36,6 +36,8 @@ exclude: !ruby/regexp '/ | [gG]emfile(?:\.lock)? | + support\/hoe\/ + | \.gemspec$ | Vagrantfile diff --git a/.travis.yml b/.travis.yml index 67363e9..f12e609 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,13 @@ --- language: ruby rvm: - - 2.3.1 - - 2.2.3 - - 2.1.6 + - 2.4.0 + - 2.3.3 + - 2.2.6 + - 2.1.9 - 2.0.0 - jruby-9.0.5.0 - - jruby-9.1.5.0 + - jruby-9.1.6.0 - 1.9.3 - 1.8.7 - ree @@ -20,12 +21,6 @@ matrix: gemfile: - Gemfile before_script: - - | - case "${TRAVIS_RUBY_VERSION}" in - rbx*) - gem install psych - ;; - esac - rake travis:before -t script: rake travis after_script: diff --git a/Gemfile b/Gemfile index 898283e..9d0186d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,14 +1,14 @@ # -*- ruby -*- # NOTE: This file is present to keep Travis CI happy. Edits to it will not -# be accepted, except to remove all of this crap later. +# be accepted. source 'https://rubygems.org/' mime_version = if RUBY_VERSION < '1.9' gem 'rdoc', '< 4.0' - # gem 'ruby-debug' + gem 'rake', '~> 10.0' '1.25' elsif RUBY_VERSION < '2.0' # gem 'debugger' if RUBY_ENGINE == 'ruby' diff --git a/History.md b/History.md index 6fd68cc..adfd992 100644 --- a/History.md +++ b/History.md @@ -8,6 +8,14 @@ `archive-tar-minitar` will install both `minitar` and `minitar-cli`, at least until version 1.0.) + * Minitar extraction before 0.6 traverses directories if the tarball + includes a relative directory reference, as reported in [#16][] by + @ecneladis. This has been disallowed entirely and will throw a + SecureRelativePathError when found. Additionally, if the final + destination of an entry is an already-existing symbolic link, the + existing symbolic link will be removed and the file will be written + correctly (on platforms that support symblic links). + * Enhancements: * Licence change. After speaking with Mauricio Fernández, we have changed @@ -51,18 +59,16 @@ * Bugs: - * Fix [#2](https://github.com/halostatue/minitar/issues/2) to handle IO - streams that are not seekable, such as pipes, STDIN, or STDOUT. - * Fix [#3](https://github.com/halostatue/minitar/issues/3) to make the - test timezone resilient. - * Fix [#4](https://github.com/halostatue/minitar/issues/4) for supporting - the reading of tar files with filenames in the GNU long filename - extension format. Ported from @atoulme’s fork, originally provided by - Curtis Sampson. - * Fix [#6](https://github.com/halostatue/minitar/issues/6) by making it - raise the correct error for a long filename with no path components. - * Fix [#14](https://github.com/halostatue/minitar/pull/6) provided by - @kzys should fix Windows detection issues. + * Fix [#2][] to handle IO streams that are not seekable, such as pipes, + STDIN, or STDOUT. + * Fix [#3][] to make the test timezone resilient. + * Fix [#4][] for supporting the reading of tar files with filenames in + the GNU long filename extension format. Ported from @atoulme’s fork, + originally provided by Curtis Sampson. + * Fix [#6][] by making it raise the correct error for a long filename + with no path components. + * Fix [#14][] provided by @kzys should fix Windows detection issues. + * Fix [#16][] as specified above. * Development: @@ -83,3 +89,10 @@ * Initial release. Does files and directories. Command does create, extract, and list. + +[#2]: https://github.com/halostatue/minitar/issues/2 +[#3]: https://github.com/halostatue/minitar/issues/3 +[#4]: https://github.com/halostatue/minitar/issues/4 +[#6]: https://github.com/halostatue/minitar/issues/6 +[#14]: https://github.com/halostatue/minitar/issues/14 +[#16]: https://github.com/halostatue/minitar/issues/16 diff --git a/Licence.md b/Licence.md index e08ed41..6d768f9 100644 --- a/Licence.md +++ b/Licence.md @@ -3,9 +3,8 @@ minitar is free software that may be redistributed and/or modified under the terms of Ruby’s licence or the Simplified BSD licence. -* Copyright 2004–2014 Austin Ziegler. +* Copyright 2004–2017 Austin Ziegler. * Portions copyright 2004 Mauricio Julio Fernández Pradier. -* Portions copyright 2001–2004 Satoru Takabayashi. ### Simplified BSD Licence diff --git a/README.rdoc b/README.rdoc index ba76d41..a621ff6 100644 --- a/README.rdoc +++ b/README.rdoc @@ -4,10 +4,9 @@ home :: https://github.com/halostatue/minitar/ code :: https://github.com/halostatue/minitar/ bugs :: https://github.com/halostatue/minitar/issues rdoc :: http://rdoc.info/gems/minitar/ -continuous integration :: {}[https://travis-ci.org/halostatue/minitar] +continuous integration :: {}[https://travis-ci.org/halostatue/minitar] {}[https://ci.appveyor.com/project/halostatue/minitar] - -test coverage :: {Coverage Status}[https://coveralls.io/r/halostatue/minitar] +test coverage :: {Coverage Status}[https://coveralls.io/r/halostatue/minitar] == Description @@ -49,7 +48,7 @@ Minitar::Output#close automatically closes both the Output object and the wrapped data stream object. begin - sgz = Zlib::GzipWriter.new(StringIO.new("")) + sgz = Zlib::GzipWriter.new(StringIO.new(String.new)) tar = Output.new(sgz) Find.find('tests') do |entry| Minitar.pack_file(entry, tar) diff --git a/Rakefile b/Rakefile index 06700d2..15e15af 100644 --- a/Rakefile +++ b/Rakefile @@ -4,17 +4,20 @@ require 'rubygems' require 'hoe' require 'rake/clean' +$LOAD_PATH.unshift('support') + Hoe.plugin :doofus Hoe.plugin :gemspec2 Hoe.plugin :git Hoe.plugin :minitest Hoe.plugin :travis +Hoe.plugin :deprecated_gem Hoe.plugin :email unless ENV['CI'] or ENV['TRAVIS'] spec = Hoe.spec 'minitar' do developer('Austin Ziegler', 'halostatue@gmail.com') - self.require_ruby_version '>= 1.8' + require_ruby_version '>= 1.8' self.history_file = 'History.md' self.readme_file = 'README.rdoc' @@ -26,8 +29,8 @@ spec = Hoe.spec 'minitar' do extra_dev_deps << ['hoe-rubygems', '~> 1.0'] extra_dev_deps << ['hoe-travis', '~> 1.2'] extra_dev_deps << ['minitest', '~> 5.3'] - extra_dev_deps << ['minitest-autotest', ['>= 1.0.b', '<2']] - extra_dev_deps << ['rake', '~> 10.0'] + extra_dev_deps << ['minitest-autotest', ['>= 1.0', '<2']] + extra_dev_deps << ['rake', '>= 10.0', '< 12'] extra_dev_deps << ['rdoc', '>= 0.0'] end diff --git a/archive-tar-minitar.gemspec b/archive-tar-minitar.gemspec index 880f1fc..bb293cf 100644 --- a/archive-tar-minitar.gemspec +++ b/archive-tar-minitar.gemspec @@ -1,15 +1,36 @@ # -*- encoding: utf-8 -*- # stub: archive-tar-minitar 0.6 ruby lib -minitar = Gem::Specification.load('minitar.gemspec') -minitar.name = 'archive-tar-minitar' -minitar.description = - minitar.summary = %q(This gem is deprecated. Just install 'minitar'.) -minitar.files.delete_if { |f| f !~ %r{lib/archive-tar-minitar\.rb} } -minitar.extra_rdoc_files.clear -minitar.rdoc_options.clear -minitar.dependencies.clear -minitar.add_dependency(%q, "~> #{minitar.version}") -minitar.add_dependency(%q, "<= 1.0") - -minitar +Gem::Specification.new do |s| + s.name = "archive-tar-minitar" + s.version = "0.6" + + s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= + s.require_paths = ["lib"] + s.authors = ["Austin Ziegler"] + s.date = "2017-02-06" + s.description = "'archive-tar-minitar' has been deprecated; just install 'minitar'. The minitar library is a pure-Ruby library that provides the ability to deal\nwith POSIX tar(1) archive files.\n\nThis is release 0.6, \u{2026}\n\nminitar (previously called Archive::Tar::Minitar) is based heavily on code\noriginally written by Mauricio Julio Fern\u{e1}ndez Pradier for the rpa-base\nproject." + s.email = ["halostatue@gmail.com"] + s.files = ["lib/archive-tar-minitar.rb"] + s.homepage = "https://github.com/halostatue/minitar/" + s.licenses = ["Ruby", "BSD-2-Clause"] + s.post_install_message = "'archive-tar-minitar' has been deprecated; just install 'minitar'." + s.required_ruby_version = Gem::Requirement.new(">= 1.8") + s.rubygems_version = "2.5.1" + s.summary = "'archive-tar-minitar' has been deprecated; just install 'minitar'." + + if s.respond_to? :specification_version then + s.specification_version = 4 + + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then + s.add_runtime_dependency(%q, ["~> 0.6"]) + s.add_runtime_dependency(%q, ["<= 1.0"]) + else + s.add_dependency(%q, ["~> 0.6"]) + s.add_dependency(%q, ["<= 1.0"]) + end + else + s.add_dependency(%q, ["~> 0.6"]) + s.add_dependency(%q, ["<= 1.0"]) + end +end diff --git a/lib/archive/tar/minitar.rb b/lib/archive/tar/minitar.rb index b69a3a1..b38bc7a 100644 --- a/lib/archive/tar/minitar.rb +++ b/lib/archive/tar/minitar.rb @@ -73,17 +73,22 @@ def modules module Archive::Tar::Minitar VERSION = '0.6' # :nodoc: + # The base class for any minitar error. + Error = Class.new(StandardError) # Raised when a wrapped data stream class is not seekable. - NonSeekableStream = Class.new(StandardError) + NonSeekableStream = Class.new(Error) # The exception raised when operations are performed on a stream that has # previously been closed. - ClosedStream = Class.new(StandardError) + ClosedStream = Class.new(Error) # The exception raised when a filename exceeds 256 bytes in length, the # maximum supported by the standard Tar format. - FileNameTooLong = Class.new(StandardError) + FileNameTooLong = Class.new(Error) # The exception raised when a data stream ends before the amount of data # expected in the archive's PosixHeader. UnexpectedEOF = Class.new(StandardError) + # The exception raised when a file contains a relative path in secure mode + # (the default for this version). + SecureRelativePathError = Class.new(Error) class << self # Tests if +path+ refers to a directory. Fixes an apparently diff --git a/lib/archive/tar/minitar/input.rb b/lib/archive/tar/minitar/input.rb index 599948e..4ba27fe 100644 --- a/lib/archive/tar/minitar/input.rb +++ b/lib/archive/tar/minitar/input.rb @@ -97,10 +97,25 @@ def extract_entry(destdir, entry) # :yields action, name, stats: :entry => entry } + # extract_entry is not vulnerable to prefix '/' vulnerabilities, but it + # is vulnerable to relative path directories. This code will break this + # vulnerability. For this version, we are breaking relative paths HARD by + # throwing an exception. + # + # Future versions may permit relative paths as long as the file does not + # leave +destdir+. + # + # However, squeeze consecutive '/' characters together. + full_name = entry.full_name.squeeze('/') + + if full_name =~ /\.{2}(?:\/|\z)/ + raise SecureRelativePathError, %q(Path contains '..') + end + if entry.directory? - dest = File.join(destdir, entry.full_name) + dest = File.join(destdir, full_name) - yield :dir, entry.full_name, stats if block_given? + yield :dir, full_name, stats if block_given? if Archive::Tar::Minitar.dir?(dest) begin @@ -109,6 +124,8 @@ def extract_entry(destdir, entry) # :yields action, name, stats: nil end else + File.unlink(dest.chomp('/')) if File.symlink?(dest.chomp('/')) + FileUtils.mkdir_p(dest, :mode => entry.mode) FileUtils.chmod(entry.mode, dest) end @@ -117,13 +134,16 @@ def extract_entry(destdir, entry) # :yields action, name, stats: fsync_dir(File.join(dest, "..")) return else # it's a file - destdir = File.join(destdir, File.dirname(entry.full_name)) + destdir = File.join(destdir, File.dirname(full_name)) FileUtils.mkdir_p(destdir, :mode => 0755) - destfile = File.join(destdir, File.basename(entry.full_name)) + destfile = File.join(destdir, File.basename(full_name)) + + File.unlink(destfile) if File.symlink?(destfile) + FileUtils.chmod(0600, destfile) rescue nil # Errno::ENOENT - yield :file_start, entry.full_name, stats if block_given? + yield :file_start, full_name, stats if block_given? File.open(destfile, "wb", entry.mode) do |os| loop do @@ -133,7 +153,7 @@ def extract_entry(destdir, entry) # :yields action, name, stats: stats[:currinc] = os.write(data) stats[:current] += stats[:currinc] - yield :file_progress, entry.full_name, stats if block_given? + yield :file_progress, full_name, stats if block_given? end os.fsync end @@ -142,7 +162,7 @@ def extract_entry(destdir, entry) # :yields action, name, stats: fsync_dir(File.dirname(destfile)) fsync_dir(File.join(File.dirname(destfile), "..")) - yield :file_done, entry.full_name, stats if block_given? + yield :file_done, full_name, stats if block_given? end end diff --git a/minitar.gemspec b/minitar.gemspec index f0674c5..556a6df 100644 --- a/minitar.gemspec +++ b/minitar.gemspec @@ -8,7 +8,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.require_paths = ["lib"] s.authors = ["Austin Ziegler"] - s.date = "2016-11-08" + s.date = "2017-02-06" s.description = "The minitar library is a pure-Ruby library that provides the ability to deal\nwith POSIX tar(1) archive files.\n\nThis is release 0.6, \u{2026}\n\nminitar (previously called Archive::Tar::Minitar) is based heavily on code\noriginally written by Mauricio Julio Fern\u{e1}ndez Pradier for the rpa-base\nproject." s.email = ["halostatue@gmail.com"] s.extra_rdoc_files = ["Code-of-Conduct.md", "Contributing.md", "History.md", "Licence.md", "Manifest.txt", "README.rdoc", "docs/bsdl.txt", "docs/ruby.txt"] @@ -30,10 +30,10 @@ Gem::Specification.new do |s| s.add_development_dependency(%q, ["~> 1.6"]) s.add_development_dependency(%q, ["~> 1.0"]) s.add_development_dependency(%q, ["~> 1.2"]) - s.add_development_dependency(%q, ["< 2", ">= 1.0.b"]) - s.add_development_dependency(%q, ["~> 10.0"]) + s.add_development_dependency(%q, ["< 2", ">= 1.0"]) + s.add_development_dependency(%q, ["< 12", ">= 10.0"]) s.add_development_dependency(%q, [">= 0.0"]) - s.add_development_dependency(%q, ["~> 3.15"]) + s.add_development_dependency(%q, ["~> 3.16"]) else s.add_dependency(%q, ["~> 5.9"]) s.add_dependency(%q, ["~> 1.0"]) @@ -41,10 +41,10 @@ Gem::Specification.new do |s| s.add_dependency(%q, ["~> 1.6"]) s.add_dependency(%q, ["~> 1.0"]) s.add_dependency(%q, ["~> 1.2"]) - s.add_dependency(%q, ["< 2", ">= 1.0.b"]) - s.add_dependency(%q, ["~> 10.0"]) + s.add_dependency(%q, ["< 2", ">= 1.0"]) + s.add_dependency(%q, ["< 12", ">= 10.0"]) s.add_dependency(%q, [">= 0.0"]) - s.add_dependency(%q, ["~> 3.15"]) + s.add_dependency(%q, ["~> 3.16"]) end else s.add_dependency(%q, ["~> 5.9"]) @@ -53,9 +53,9 @@ Gem::Specification.new do |s| s.add_dependency(%q, ["~> 1.6"]) s.add_dependency(%q, ["~> 1.0"]) s.add_dependency(%q, ["~> 1.2"]) - s.add_dependency(%q, ["< 2", ">= 1.0.b"]) - s.add_dependency(%q, ["~> 10.0"]) + s.add_dependency(%q, ["< 2", ">= 1.0"]) + s.add_dependency(%q, ["< 12", ">= 10.0"]) s.add_dependency(%q, [">= 0.0"]) - s.add_dependency(%q, ["~> 3.15"]) + s.add_dependency(%q, ["~> 3.16"]) end end diff --git a/support/hoe/deprecated_gem.rb b/support/hoe/deprecated_gem.rb new file mode 100644 index 0000000..0d0245d --- /dev/null +++ b/support/hoe/deprecated_gem.rb @@ -0,0 +1,39 @@ +module Hoe::Deprecated_Gem + def linked_spec(spec) + atm = YAML.load(YAML.dump(spec)) + atm.name = 'archive-tar-minitar' + d = %Q('#{atm.name}' has been deprecated; just install '#{spec.name}'.) + atm.description = "#{d} #{spec.description}" + atm.summary = atm.post_install_message = d + atm.files.delete_if { |f| f !~ %r{lib/archive-tar-minitar\.rb} } + atm.extra_rdoc_files.clear + atm.rdoc_options.clear + atm.dependencies.clear + atm.add_dependency(%Q(#{spec.name}), "~> #{spec.version}") + atm.add_dependency(%Q(#{spec.name}-cli), "<= 1.0") + + unless @include_all + [ :signing_key, :cert_chain ].each { |name| + atm.send("#{name}=".to_sym, atm.default_value(name)) + } + end + + atm + end + + def define_deprecated_gem_tasks + gemspec = spec.name + '.gemspec' + atmspec = 'archive-tar-minitar.gemspec' + + file atmspec => gemspec do + open(atmspec, 'w') { |f| f.write(linked_spec(spec).to_ruby) } + end + + task :gemspec => atmspec + + Gem::PackageTask.new linked_spec(spec) do |pkg| + pkg.need_tar = @need_tar + pkg.need_zip = @need_zip + end + end +end diff --git a/test/test_tar_input.rb b/test/test_tar_input.rb index 87dc4da..caffb8f 100644 --- a/test/test_tar_input.rb +++ b/test/test_tar_input.rb @@ -73,6 +73,7 @@ def test_each_works outer += 1 end + assert_equal(2, outer) end end @@ -131,4 +132,38 @@ def test_extract_entry_works assert_equal(2, outer_count) end end + + def test_extract_entry_breaks_symlinks + return if Minitar.windows? + + IO.respond_to?(:write) && + IO.write('data__/file4', '') || + File.open('data__/file4', 'w') { |f| f.write '' } + + File.symlink('data__/file4', 'data__/file3') + File.symlink('data__/file4', 'data__/data') + + Minitar.unpack(Zlib::GzipReader.new(StringIO.new(TEST_TGZ)), 'data__') + Minitar.unpack(Zlib::GzipReader.new(File.open('data__/data.tar.gz', 'rb')), + 'data__') + + refute File.symlink?('data__/file3') + refute File.symlink?('data__/data') + end + + RELATIVE_DIRECTORY_TGZ = Base64.decode64 <<-EOS +H4sICIIoKVgCA2JhZC1kaXIudGFyANPT0y8sTy0qqWSgHTAwMDAzMVEA0eZmpmDawAjChwEFQ2MDQyMg +MDUzVDAwNDY0N2VQMGCgAygtLkksAjolEcjIzMOtDqgsLQ2/J0H+gNOjYBSMglEwyAEA2LchrwAGAAA= + EOS + + def test_extract_entry_fails_with_relative_directory + reader = Zlib::GzipReader.new(StringIO.new(RELATIVE_DIRECTORY_TGZ)) + Minitar::Input.open(reader) do |stream| + stream.each do |entry| + assert_raises Archive::Tar::Minitar::SecureRelativePathError do + stream.extract_entry("data__", entry) + end + end + end + end end