Disable symlinks and check for path traversal

This commit is contained in:
John Lees-Miller 2018-08-26 12:32:18 +01:00
parent ffebfa3418
commit 3dd165b494
6 changed files with 46 additions and 40 deletions

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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