Skip to content

Commit 0e69e4b

Browse files
committed
Switched from boolean method to verification method that thorw different exception for each case
1 parent 5aab49f commit 0e69e4b

File tree

2 files changed

+59
-17
lines changed

2 files changed

+59
-17
lines changed

lib/bootstrap/util/compress.rb

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def extract(source, target, pattern = nil)
3636
raise CompressError.new("Directory #{target} exist") if ::File.exist?(target)
3737
::Zip::File.open(source) do |zip_file|
3838
zip_file.each do |file|
39-
raise CompressError.new("Extracting file outside target directory: #{file.name}") unless LogStash::Util.name_safe?(file.name)
39+
LogStash::Util.verify_name_safe!(file.name)
4040
path = ::File.join(target, file.name)
4141
FileUtils.mkdir_p(::File.dirname(path))
4242
zip_file.extract(file, path) if pattern.nil? || pattern =~ file.name
@@ -73,7 +73,7 @@ def extract(file, target)
7373
Zlib::GzipReader.open(file) do |gzip_file|
7474
::Gem::Package::TarReader.new(gzip_file) do |tar_file|
7575
tar_file.each do |entry|
76-
raise CompressError.new("Extracting file outside target directory: #{entry.full_name}") unless LogStash::Util.name_safe?(entry.full_name)
76+
LogStash::Util.verify_name_safe!(entry.full_name)
7777
target_path = ::File.join(target, entry.full_name)
7878

7979
if entry.directory?
@@ -134,21 +134,22 @@ def gzip(path, target_file)
134134
end
135135
end
136136

137-
#TODO handle symlink case, this is a bit more complex as we need to check the real path of the file
138-
# to be extracted and make sure it is still in the target directory. This is not handled by Zip::File and is a known security issue:
139-
#
140-
# Returns true if the path string is safe (no path traversal, no absolute path).
141-
# A path is safe when it is relative and does not contain `..` segments that
142-
# could escape the target directory. Works on both Unix and Windows.
143-
# @param path [String] path string to validate
144-
# @return [Boolean] true if safe, false if absolute or relative-with-escape
145-
def self.name_safe?(name)
146-
return false if name.nil? || name.to_s.strip.empty?
147-
pn = Pathname.new(name)
148-
cleanpath = pn.cleanpath
149-
return false if cleanpath.absolute?
150-
return false if cleanpath.each_filename.to_a.include?("..")
151-
true
137+
# Verifies that a path string is safe for extraction (relative, no `..` traversal).
138+
# Raises CompressError with a specific message if the path is nil/empty, absolute, or
139+
# contains `..`. Does NOT handle symlinks. Works on both Unix and Windows.
140+
# @param name [String] path string to validate
141+
# @raise [CompressError] if path is nil, empty, absolute, or traverses with `..`
142+
def self.verify_name_safe!(name)
143+
if name.nil? || name.to_s.strip.empty?
144+
raise CompressError.new("Refusing to extract file to unsafe path. Path cannot be nil or empty.")
145+
end
146+
cleanpath = Pathname.new(name).cleanpath
147+
if cleanpath.absolute?
148+
raise CompressError.new("Refusing to extract file to unsafe path: #{name}. Absolute paths are not allowed.")
149+
end
150+
if cleanpath.each_filename.to_a.include?("..")
151+
raise CompressError.new("Refusing to extract file to unsafe path: #{name}. Files may not traverse with `..`")
152+
end
152153
end
153154
end
154155
end

spec/unit/util/compress_spec.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,33 @@ def list_files(target)
5353
Dir.glob(::File.join(target, "**", "*")).select { |f| ::File.file?(f) }.size
5454
end
5555

56+
describe LogStash::Util do
57+
describe ".verify_name_safe!" do
58+
it "raises CompressError for nil or empty path" do
59+
expect { LogStash::Util.verify_name_safe!(nil) }.to raise_error(LogStash::CompressError, /Path cannot be nil or empty/)
60+
expect { LogStash::Util.verify_name_safe!("") }.to raise_error(LogStash::CompressError, /Path cannot be nil or empty/)
61+
expect { LogStash::Util.verify_name_safe!(" ") }.to raise_error(LogStash::CompressError, /Path cannot be nil or empty/)
62+
end
63+
64+
it "raises CompressError for absolute paths" do
65+
expect { LogStash::Util.verify_name_safe!("/etc/passwd") }.to raise_error(LogStash::CompressError, /Absolute paths are not allowed/)
66+
expect { LogStash::Util.verify_name_safe!("/foo/bar") }.to raise_error(LogStash::CompressError, /Absolute paths are not allowed/)
67+
end
68+
69+
it "raises CompressError for relative paths that traverse with .." do
70+
expect { LogStash::Util.verify_name_safe!("../foo") }.to raise_error(LogStash::CompressError, /Files may not traverse with `..`/)
71+
expect { LogStash::Util.verify_name_safe!("..") }.to raise_error(LogStash::CompressError, /Files may not traverse with `..`/)
72+
expect { LogStash::Util.verify_name_safe!("a/../../etc") }.to raise_error(LogStash::CompressError, /Files may not traverse with `..`/)
73+
end
74+
75+
it "does not raise for safe relative paths" do
76+
expect { LogStash::Util.verify_name_safe!("foo/bar") }.not_to raise_error
77+
expect { LogStash::Util.verify_name_safe!("a/b/c") }.not_to raise_error
78+
expect { LogStash::Util.verify_name_safe!("single") }.not_to raise_error
79+
end
80+
end
81+
end
82+
5683
describe LogStash::Util::Zip do
5784
subject { Class.new { extend LogStash::Util::Zip } }
5885

@@ -79,6 +106,12 @@ def list_files(target)
79106
subject.extract(source, target)
80107
end
81108

109+
it "raises CompressError when an entry path traverses with .." do
110+
zip_with_evil = [OpenStruct.new(:name => "../evil")]
111+
allow(Zip::File).to receive(:open).with(source).and_yield(zip_with_evil)
112+
expect { subject.extract(source, target) }.to raise_error(LogStash::CompressError, /Refusing to extract file to unsafe path.*Files may not traverse with `..`/)
113+
end
114+
82115
context "patterns" do
83116
# Theses tests sound duplicated but they are actually better than the other one
84117
# since they do not involve any mocks.
@@ -177,6 +210,14 @@ def list_files(target)
177210
expect(File).to receive(:open).exactly(3).times
178211
subject.extract(source, target)
179212
end
213+
214+
it "raises CompressError when an entry path traverses with .." do
215+
tar_with_evil = [OpenStruct.new(:full_name => "../evil", :directory? => false, :read => "")]
216+
allow(Zlib::GzipReader).to receive(:open).with(source).and_yield(gzip_file)
217+
allow(Gem::Package::TarReader).to receive(:new).with(gzip_file).and_yield(tar_with_evil)
218+
expect(FileUtils).to receive(:mkdir).with(target)
219+
expect { subject.extract(source, target) }.to raise_error(LogStash::CompressError, /Refusing to extract file to unsafe path.*Files may not traverse with `..`/)
220+
end
180221
end
181222

182223
context "#compression" do

0 commit comments

Comments
 (0)