Disable symlinks and check for path traversal
This commit is contained in:
parent
ffebfa3418
commit
3dd165b494
|
@ -109,6 +109,16 @@ module Zip
|
||||||
@name.end_with?('/')
|
@name.end_with?('/')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Is the name a relative path, free of `..` patterns that could lead to
|
||||||
|
# path traversal attacks? This does NOT handle symlinks; if the path
|
||||||
|
# contains symlinks, this check is NOT enough to guarantee safety.
|
||||||
|
def name_safe?
|
||||||
|
cleanpath = Pathname.new(@name).cleanpath
|
||||||
|
return false unless cleanpath.relative?
|
||||||
|
naive_expanded_path = ::File.join(Dir.pwd, cleanpath.to_s)
|
||||||
|
cleanpath.expand_path.to_s == naive_expanded_path
|
||||||
|
end
|
||||||
|
|
||||||
def local_entry_offset #:nodoc:all
|
def local_entry_offset #:nodoc:all
|
||||||
local_header_offset + @local_header_size
|
local_header_offset + @local_header_size
|
||||||
end
|
end
|
||||||
|
@ -147,15 +157,11 @@ module Zip
|
||||||
end
|
end
|
||||||
|
|
||||||
# Extracts entry to file dest_path (defaults to @name).
|
# Extracts entry to file dest_path (defaults to @name).
|
||||||
|
# NB: The caller is responsible for making sure dest_path is safe, if it
|
||||||
|
# is passed.
|
||||||
def extract(dest_path = nil, &block)
|
def extract(dest_path = nil, &block)
|
||||||
if dest_path.nil? && Pathname.new(@name).absolute?
|
if dest_path.nil? && !name_safe?
|
||||||
puts "WARNING: skipped absolute path in #{@name}"
|
puts "WARNING: skipped #{@name} as unsafe"
|
||||||
return self
|
|
||||||
elsif @name.squeeze('/') =~ /\.{2}(?:\/|\z)/
|
|
||||||
puts "WARNING: skipped \"../\" path component(s) in #{@name}"
|
|
||||||
return self
|
|
||||||
elsif symlink? && get_input_stream.read =~ %r{../..}
|
|
||||||
puts "WARNING: skipped \"#{get_input_stream.read}\" symlink path in #{@name}"
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -620,32 +626,9 @@ module Zip
|
||||||
|
|
||||||
# BUG: create_symlink() does not use &block
|
# BUG: create_symlink() does not use &block
|
||||||
def create_symlink(dest_path)
|
def create_symlink(dest_path)
|
||||||
stat = nil
|
# TODO: Symlinks pose security challenges. Symlink support temporarily
|
||||||
begin
|
# removed in view of https://github.com/rubyzip/rubyzip/issues/369 .
|
||||||
stat = ::File.lstat(dest_path)
|
puts "WARNING: skipped symlink #{dest_path}"
|
||||||
rescue Errno::ENOENT
|
|
||||||
end
|
|
||||||
|
|
||||||
io = get_input_stream
|
|
||||||
linkto = io.read
|
|
||||||
|
|
||||||
if stat
|
|
||||||
if stat.symlink?
|
|
||||||
if ::File.readlink(dest_path) == linkto
|
|
||||||
return
|
|
||||||
else
|
|
||||||
raise ::Zip::DestinationFileExistsError,
|
|
||||||
"Cannot create symlink '#{dest_path}'. " \
|
|
||||||
'A symlink already exists with that name'
|
|
||||||
end
|
|
||||||
else
|
|
||||||
raise ::Zip::DestinationFileExistsError,
|
|
||||||
"Cannot create symlink '#{dest_path}'. " \
|
|
||||||
'A file already exists with that name'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
::File.symlink(linkto, dest_path)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# apply missing data from the zip64 extra information field, if present
|
# apply missing data from the zip64 extra information field, if present
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Based on 'relative2' in https://github.com/jwilk/path-traversal-samples,
|
||||||
|
# but create the local `tmp` folder before adding the symlink. Otherwise
|
||||||
|
# we may bail out before we get to trying to create the file.
|
||||||
|
all: relative1.zip
|
||||||
|
relative1.zip:
|
||||||
|
rm -f $(@)
|
||||||
|
mkdir -p -m 755 tmp/tmp
|
||||||
|
umask 022 && echo moo > moo
|
||||||
|
cd tmp && zip -X ../$(@) tmp tmp/../../moo
|
||||||
|
rm -rf tmp moo
|
Binary file not shown.
|
@ -1,3 +1,3 @@
|
||||||
# Path Traversal Samples
|
# Path Traversal Samples
|
||||||
|
|
||||||
Copied from https://github.com/jwilk/path-traversal-samples on 2018-08-25.
|
Copied from https://github.com/tuzovakaoff/zip_path_traversal on 2018-08-25.
|
||||||
|
|
Binary file not shown.
|
@ -47,7 +47,15 @@ class PathTraversalTest < MiniTest::Test
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_non_leading_dot_dot
|
def test_non_leading_dot_dot_with_existing_folder
|
||||||
|
in_tmpdir do
|
||||||
|
extract_path_traversal_zip 'relative1.zip'
|
||||||
|
assert Dir.exist?('tmp')
|
||||||
|
refute File.exist?('../moo')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_non_leading_dot_dot_without_existing_folder
|
||||||
in_tmpdir do
|
in_tmpdir do
|
||||||
extract_path_traversal_zip 'jwilk/relative2.zip'
|
extract_path_traversal_zip 'jwilk/relative2.zip'
|
||||||
refute File.exist?('../moo')
|
refute File.exist?('../moo')
|
||||||
|
@ -64,7 +72,10 @@ class PathTraversalTest < MiniTest::Test
|
||||||
|
|
||||||
def test_directory_symlink
|
def test_directory_symlink
|
||||||
in_tmpdir do
|
in_tmpdir do
|
||||||
extract_path_traversal_zip 'jwilk/dirsymlink.zip'
|
# Can't create tmp/moo, because the tmp symlink is skipped.
|
||||||
|
assert_raises Errno::ENOENT do
|
||||||
|
extract_path_traversal_zip 'jwilk/dirsymlink.zip'
|
||||||
|
end
|
||||||
refute File.exist?('/tmp/moo')
|
refute File.exist?('/tmp/moo')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -83,9 +94,11 @@ class PathTraversalTest < MiniTest::Test
|
||||||
|
|
||||||
def test_two_directory_symlinks_b
|
def test_two_directory_symlinks_b
|
||||||
in_tmpdir do
|
in_tmpdir do
|
||||||
extract_path_traversal_zip 'jwilk/dirsymlink2b.zip'
|
# Can't create par/moo, because the symlinks are skipped.
|
||||||
assert File.exist?('cur')
|
assert_raises Errno::ENOENT do
|
||||||
assert_equal '.', File.readlink('cur')
|
extract_path_traversal_zip 'jwilk/dirsymlink2b.zip'
|
||||||
|
end
|
||||||
|
refute File.exist?('cur')
|
||||||
refute File.exist?('../moo')
|
refute File.exist?('../moo')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue