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?('/')
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
local_header_offset + @local_header_size
end
@ -147,15 +157,11 @@ module Zip
end
# 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)
if dest_path.nil? && Pathname.new(@name).absolute?
puts "WARNING: skipped absolute path in #{@name}"
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}"
if dest_path.nil? && !name_safe?
puts "WARNING: skipped #{@name} as unsafe"
return self
end
@ -620,32 +626,9 @@ module Zip
# BUG: create_symlink() does not use &block
def create_symlink(dest_path)
stat = nil
begin
stat = ::File.lstat(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)
# TODO: Symlinks pose security challenges. Symlink support temporarily
# removed in view of https://github.com/rubyzip/rubyzip/issues/369 .
puts "WARNING: skipped symlink #{dest_path}"
end
# 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
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
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
extract_path_traversal_zip 'jwilk/relative2.zip'
refute File.exist?('../moo')
@ -64,7 +72,10 @@ class PathTraversalTest < MiniTest::Test
def test_directory_symlink
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')
end
end
@ -83,9 +94,11 @@ class PathTraversalTest < MiniTest::Test
def test_two_directory_symlinks_b
in_tmpdir do
extract_path_traversal_zip 'jwilk/dirsymlink2b.zip'
assert File.exist?('cur')
assert_equal '.', File.readlink('cur')
# Can't create par/moo, because the symlinks are skipped.
assert_raises Errno::ENOENT do
extract_path_traversal_zip 'jwilk/dirsymlink2b.zip'
end
refute File.exist?('cur')
refute File.exist?('../moo')
end
end