Skip to content

Commit e25205e

Browse files
committed
Resolve relative path vulnerability
Fixes #16
1 parent 27569df commit e25205e

File tree

5 files changed

+93
-23
lines changed

5 files changed

+93
-23
lines changed

History.md

+25-12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
`archive-tar-minitar` will install both `minitar` and `minitar-cli`, at
99
least until version 1.0.)
1010

11+
* Minitar extraction before 0.6 traverses directories if the tarball
12+
includes a relative directory reference, as reported in [#16][] by
13+
@ecneladis. This has been disallowed entirely and will throw a
14+
SecureRelativePathError when found. Additionally, if the final
15+
destination of an entry is an already-existing symbolic link, the
16+
existing symbolic link will be removed and the file will be written
17+
correctly (on platforms that support symblic links).
18+
1119
* Enhancements:
1220

1321
* Licence change. After speaking with Mauricio Fernández, we have changed
@@ -51,18 +59,16 @@
5159

5260
* Bugs:
5361

54-
* Fix [#2](https://github.com/halostatue/minitar/issues/2) to handle IO
55-
streams that are not seekable, such as pipes, STDIN, or STDOUT.
56-
* Fix [#3](https://github.com/halostatue/minitar/issues/3) to make the
57-
test timezone resilient.
58-
* Fix [#4](https://github.com/halostatue/minitar/issues/4) for supporting
59-
the reading of tar files with filenames in the GNU long filename
60-
extension format. Ported from @atoulme’s fork, originally provided by
61-
Curtis Sampson.
62-
* Fix [#6](https://github.com/halostatue/minitar/issues/6) by making it
63-
raise the correct error for a long filename with no path components.
64-
* Fix [#14](https://github.com/halostatue/minitar/pull/6) provided by
65-
@kzys should fix Windows detection issues.
62+
* Fix [#2][] to handle IO streams that are not seekable, such as pipes,
63+
STDIN, or STDOUT.
64+
* Fix [#3][] to make the test timezone resilient.
65+
* Fix [#4][] for supporting the reading of tar files with filenames in
66+
the GNU long filename extension format. Ported from @atoulme’s fork,
67+
originally provided by Curtis Sampson.
68+
* Fix [#6][] by making it raise the correct error for a long filename
69+
with no path components.
70+
* Fix [#14][] provided by @kzys should fix Windows detection issues.
71+
* Fix [#16][] as specified above.
6672

6773
* Development:
6874

@@ -83,3 +89,10 @@
8389

8490
* Initial release. Does files and directories. Command does create, extract,
8591
and list.
92+
93+
[#2]: https://github.com/halostatue/minitar/issues/2
94+
[#3]: https://github.com/halostatue/minitar/issues/3
95+
[#4]: https://github.com/halostatue/minitar/issues/4
96+
[#6]: https://github.com/halostatue/minitar/issues/6
97+
[#14]: https://github.com/halostatue/minitar/issues/14
98+
[#16]: https://github.com/halostatue/minitar/issues/16

lib/archive/tar/minitar.rb

+8-3
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,22 @@ def modules
7373
module Archive::Tar::Minitar
7474
VERSION = '0.6' # :nodoc:
7575

76+
# The base class for any minitar error.
77+
Error = Class.new(StandardError)
7678
# Raised when a wrapped data stream class is not seekable.
77-
NonSeekableStream = Class.new(StandardError)
79+
NonSeekableStream = Class.new(Error)
7880
# The exception raised when operations are performed on a stream that has
7981
# previously been closed.
80-
ClosedStream = Class.new(StandardError)
82+
ClosedStream = Class.new(Error)
8183
# The exception raised when a filename exceeds 256 bytes in length, the
8284
# maximum supported by the standard Tar format.
83-
FileNameTooLong = Class.new(StandardError)
85+
FileNameTooLong = Class.new(Error)
8486
# The exception raised when a data stream ends before the amount of data
8587
# expected in the archive's PosixHeader.
8688
UnexpectedEOF = Class.new(StandardError)
89+
# The exception raised when a file contains a relative path in secure mode
90+
# (the default for this version).
91+
SecureRelativePathError = Class.new(Error)
8792

8893
class << self
8994
# Tests if +path+ refers to a directory. Fixes an apparently

lib/archive/tar/minitar/input.rb

+27-7
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,25 @@ def extract_entry(destdir, entry) # :yields action, name, stats:
9797
:entry => entry
9898
}
9999

100+
# extract_entry is not vulnerable to prefix '/' vulnerabilities, but it
101+
# is vulnerable to relative path directories. This code will break this
102+
# vulnerability. For this version, we are breaking relative paths HARD by
103+
# throwing an exception.
104+
#
105+
# Future versions may permit relative paths as long as the file does not
106+
# leave +destdir+.
107+
#
108+
# However, squeeze consecutive '/' characters together.
109+
full_name = entry.full_name.squeeze('/')
110+
111+
if full_name =~ /\.{2}(?:\/|\z)/
112+
raise SecureRelativePathError, %q(Path contains '..')
113+
end
114+
100115
if entry.directory?
101-
dest = File.join(destdir, entry.full_name)
116+
dest = File.join(destdir, full_name)
102117

103-
yield :dir, entry.full_name, stats if block_given?
118+
yield :dir, full_name, stats if block_given?
104119

105120
if Archive::Tar::Minitar.dir?(dest)
106121
begin
@@ -109,6 +124,8 @@ def extract_entry(destdir, entry) # :yields action, name, stats:
109124
nil
110125
end
111126
else
127+
File.unlink(dest.chomp('/')) if File.symlink?(dest.chomp('/'))
128+
112129
FileUtils.mkdir_p(dest, :mode => entry.mode)
113130
FileUtils.chmod(entry.mode, dest)
114131
end
@@ -117,13 +134,16 @@ def extract_entry(destdir, entry) # :yields action, name, stats:
117134
fsync_dir(File.join(dest, ".."))
118135
return
119136
else # it's a file
120-
destdir = File.join(destdir, File.dirname(entry.full_name))
137+
destdir = File.join(destdir, File.dirname(full_name))
121138
FileUtils.mkdir_p(destdir, :mode => 0755)
122139

123-
destfile = File.join(destdir, File.basename(entry.full_name))
140+
destfile = File.join(destdir, File.basename(full_name))
141+
142+
File.unlink(destfile) if File.symlink?(destfile)
143+
124144
FileUtils.chmod(0600, destfile) rescue nil # Errno::ENOENT
125145

126-
yield :file_start, entry.full_name, stats if block_given?
146+
yield :file_start, full_name, stats if block_given?
127147

128148
File.open(destfile, "wb", entry.mode) do |os|
129149
loop do
@@ -133,7 +153,7 @@ def extract_entry(destdir, entry) # :yields action, name, stats:
133153
stats[:currinc] = os.write(data)
134154
stats[:current] += stats[:currinc]
135155

136-
yield :file_progress, entry.full_name, stats if block_given?
156+
yield :file_progress, full_name, stats if block_given?
137157
end
138158
os.fsync
139159
end
@@ -142,7 +162,7 @@ def extract_entry(destdir, entry) # :yields action, name, stats:
142162
fsync_dir(File.dirname(destfile))
143163
fsync_dir(File.join(File.dirname(destfile), ".."))
144164

145-
yield :file_done, entry.full_name, stats if block_given?
165+
yield :file_done, full_name, stats if block_given?
146166
end
147167
end
148168

minitar.gemspec

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Gem::Specification.new do |s|
88
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
99
s.require_paths = ["lib"]
1010
s.authors = ["Austin Ziegler"]
11-
s.date = "2016-11-08"
11+
s.date = "2016-11-14"
1212
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."
1313
s.email = ["[email protected]"]
1414
s.extra_rdoc_files = ["Code-of-Conduct.md", "Contributing.md", "History.md", "Licence.md", "Manifest.txt", "README.rdoc", "docs/bsdl.txt", "docs/ruby.txt"]

test/test_tar_input.rb

+32
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def test_each_works
7373

7474
outer += 1
7575
end
76+
7677
assert_equal(2, outer)
7778
end
7879
end
@@ -131,4 +132,35 @@ def test_extract_entry_works
131132
assert_equal(2, outer_count)
132133
end
133134
end
135+
136+
def test_extract_entry_breaks_symlinks
137+
return if Minitar.windows?
138+
139+
IO.write('data__/file4', '')
140+
File.symlink('data__/file4', 'data__/file3')
141+
File.symlink('data__/file4', 'data__/data')
142+
143+
Minitar.unpack(Zlib::GzipReader.new(StringIO.new(TEST_TGZ)), 'data__')
144+
Minitar.unpack(Zlib::GzipReader.new(File.open('data__/data.tar.gz', 'rb')),
145+
'data__')
146+
147+
refute File.symlink?('data__/file3')
148+
refute File.symlink?('data__/data')
149+
end
150+
151+
RELATIVE_DIRECTORY_TGZ = Base64.decode64 <<-EOS
152+
H4sICIIoKVgCA2JhZC1kaXIudGFyANPT0y8sTy0qqWSgHTAwMDAzMVEA0eZmpmDawAjChwEFQ2MDQyMg
153+
MDUzVDAwNDY0N2VQMGCgAygtLkksAjolEcjIzMOtDqgsLQ2/J0H+gNOjYBSMglEwyAEA2LchrwAGAAA=
154+
EOS
155+
156+
def test_extract_entry_fails_with_relative_directory
157+
reader = Zlib::GzipReader.new(StringIO.new(RELATIVE_DIRECTORY_TGZ))
158+
Minitar::Input.open(reader) do |stream|
159+
stream.each do |entry|
160+
assert_raises Archive::Tar::Minitar::SecureRelativePathError do
161+
stream.extract_entry("data__", entry)
162+
end
163+
end
164+
end
165+
end
134166
end

0 commit comments

Comments
 (0)