Skip to content

Commit

Permalink
Resolve relative path vulnerability
Browse files Browse the repository at this point in the history
  • Loading branch information
halostatue committed Feb 5, 2017
1 parent 27569df commit 2128df5
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 41 deletions.
15 changes: 5 additions & 10 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand Down
37 changes: 25 additions & 12 deletions History.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand All @@ -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
4 changes: 2 additions & 2 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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'
Expand All @@ -27,7 +27,7 @@ spec = Hoe.spec 'minitar' do
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 << ['rake', '>= 10.0', '< 12']
extra_dev_deps << ['rdoc', '>= 0.0']
end

Expand Down
11 changes: 8 additions & 3 deletions lib/archive/tar/minitar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 27 additions & 7 deletions lib/archive/tar/minitar/input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down
14 changes: 7 additions & 7 deletions minitar.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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-05"
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"]
Expand All @@ -31,9 +31,9 @@ Gem::Specification.new do |s|
s.add_development_dependency(%q<hoe-rubygems>, ["~> 1.0"])
s.add_development_dependency(%q<hoe-travis>, ["~> 1.2"])
s.add_development_dependency(%q<minitest-autotest>, ["< 2", ">= 1.0.b"])
s.add_development_dependency(%q<rake>, ["~> 10.0"])
s.add_development_dependency(%q<rake>, ["< 12", ">= 10.0"])
s.add_development_dependency(%q<rdoc>, [">= 0.0"])
s.add_development_dependency(%q<hoe>, ["~> 3.15"])
s.add_development_dependency(%q<hoe>, ["~> 3.16"])
else
s.add_dependency(%q<minitest>, ["~> 5.9"])
s.add_dependency(%q<hoe-doofus>, ["~> 1.0"])
Expand All @@ -42,9 +42,9 @@ Gem::Specification.new do |s|
s.add_dependency(%q<hoe-rubygems>, ["~> 1.0"])
s.add_dependency(%q<hoe-travis>, ["~> 1.2"])
s.add_dependency(%q<minitest-autotest>, ["< 2", ">= 1.0.b"])
s.add_dependency(%q<rake>, ["~> 10.0"])
s.add_dependency(%q<rake>, ["< 12", ">= 10.0"])
s.add_dependency(%q<rdoc>, [">= 0.0"])
s.add_dependency(%q<hoe>, ["~> 3.15"])
s.add_dependency(%q<hoe>, ["~> 3.16"])
end
else
s.add_dependency(%q<minitest>, ["~> 5.9"])
Expand All @@ -54,8 +54,8 @@ Gem::Specification.new do |s|
s.add_dependency(%q<hoe-rubygems>, ["~> 1.0"])
s.add_dependency(%q<hoe-travis>, ["~> 1.2"])
s.add_dependency(%q<minitest-autotest>, ["< 2", ">= 1.0.b"])
s.add_dependency(%q<rake>, ["~> 10.0"])
s.add_dependency(%q<rake>, ["< 12", ">= 10.0"])
s.add_dependency(%q<rdoc>, [">= 0.0"])
s.add_dependency(%q<hoe>, ["~> 3.15"])
s.add_dependency(%q<hoe>, ["~> 3.16"])
end
end
32 changes: 32 additions & 0 deletions test/test_tar_input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def test_each_works

outer += 1
end

assert_equal(2, outer)
end
end
Expand Down Expand Up @@ -131,4 +132,35 @@ def test_extract_entry_works
assert_equal(2, outer_count)
end
end

def test_extract_entry_breaks_symlinks
return if Minitar.windows?

IO.write('data__/file4', '')
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

0 comments on commit 2128df5

Please # to comment.