From 6e0d23178a39f1b9ee0debc4fffb6d90994c6955 Mon Sep 17 00:00:00 2001 From: Bart de Water Date: Sun, 1 Jul 2018 14:57:50 -0400 Subject: [PATCH 01/11] Fix CVE-2018-1000544 absolute path traversal Small refactor along the way to centralize destination handling when no explicit path is given and a potential malicious one from the zipfile is used --- lib/zip/entry.rb | 12 ++++++++---- test/data/absolutepath.zip | Bin 0 -> 289 bytes test/entry_test.rb | 26 ++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 test/data/absolutepath.zip diff --git a/lib/zip/entry.rb b/lib/zip/entry.rb index 4d8e1751..37222a52 100644 --- a/lib/zip/entry.rb +++ b/lib/zip/entry.rb @@ -147,14 +147,18 @@ def next_header_offset #:nodoc:all end # Extracts entry to file dest_path (defaults to @name). - def extract(dest_path = @name, &block) - block ||= proc { ::Zip.on_exists_proc } - - if @name.squeeze('/') =~ /\.{2}(?:\/|\z)/ + 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 end + dest_path ||= @name + block ||= proc { ::Zip.on_exists_proc } + if directory? || file? || symlink? __send__("create_#{@ftype}", dest_path, &block) else diff --git a/test/data/absolutepath.zip b/test/data/absolutepath.zip new file mode 100644 index 0000000000000000000000000000000000000000..59fceed7d1e59f2323afa67c0a0b175c5dfd2489 GIT binary patch literal 289 zcmWIWW@h1H0D*No&-#EFP=b{~fI+_`w?IEMf`>t}O)>g;m16W)MiGVppcWAZ4j|l$ zriB-#B`q^2Rj;I?1f;d28K@1aRoio|#zQ8c3G5u|x89hu1Fd2O;s9?(CQ)Ww&ftN$ z%HgdehzWHR2i#E*?Fc6!8_mmr(7vS67^WTJKn}PA1H4(;K;|<6VH=Qc1#uVv>aI9M literal 0 HcmV?d00001 diff --git a/test/entry_test.rb b/test/entry_test.rb index b49783d3..a75052e3 100644 --- a/test/entry_test.rb +++ b/test/entry_test.rb @@ -151,4 +151,30 @@ def test_store_file_without_compression assert_match(/mimetypeapplication\/epub\+zip/, first_100_bytes) end + + def test_entry_name_with_absolute_path_does_not_extract + path = '/tmp/file.txt' + File.delete(path) if File.exist?(path) + + Zip::File.open('test/data/absolutepath.zip') do |zip_file| + zip_file.each do |entry| + entry.extract + end + end + + refute File.exist?(path) + end + + def test_entry_name_with_absolute_path_extract_when_given_different_path + path = '/tmp/CVE-2018-1000544' + FileUtils.rm_rf(path) if Dir.exist?(path) + + Zip::File.open('test/data/absolutepath.zip') do |zip_file| + zip_file.each do |entry| + entry.extract("#{path}/#{entry.name}") + end + end + + assert File.exist?("#{path}/tmp/file.txt") + end end From 8e78311d670ba70476fb46062c988849a82d1e02 Mon Sep 17 00:00:00 2001 From: Bart de Water Date: Sun, 1 Jul 2018 16:45:06 -0400 Subject: [PATCH 02/11] Fix CVE-2018-1000544 symlink path traversal Not sure if the exception is the right way to go --- lib/zip/entry.rb | 3 +++ test/data/symlink.zip | Bin 0 -> 330 bytes test/entry_test.rb | 10 ++++++++++ 3 files changed, 13 insertions(+) create mode 100644 test/data/symlink.zip diff --git a/lib/zip/entry.rb b/lib/zip/entry.rb index 37222a52..28d60091 100644 --- a/lib/zip/entry.rb +++ b/lib/zip/entry.rb @@ -154,6 +154,9 @@ def extract(dest_path = nil, &block) 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 end dest_path ||= @name diff --git a/test/data/symlink.zip b/test/data/symlink.zip new file mode 100644 index 0000000000000000000000000000000000000000..e74ee19ab39d8e1e97d84522ff3209bda4ca1ad4 GIT binary patch literal 330 zcmWIWW@h1H0D$_BEQ2?*x`>FFR20|3>Y BIivsp literal 0 HcmV?d00001 diff --git a/test/entry_test.rb b/test/entry_test.rb index a75052e3..eaa9c0d9 100644 --- a/test/entry_test.rb +++ b/test/entry_test.rb @@ -177,4 +177,14 @@ def test_entry_name_with_absolute_path_extract_when_given_different_path assert File.exist?("#{path}/tmp/file.txt") end + + def test_entry_name_with_relative_symlink + assert_raises Errno::ENOENT do + Zip::File.open('test/data/symlink.zip') do |zip_file| + zip_file.each do |entry| + entry.extract + end + end + end + end end From cf7158344c65a67dc5f18bf589a6b742e3452f45 Mon Sep 17 00:00:00 2001 From: Vipul A M Date: Sun, 26 Aug 2018 11:36:08 +0900 Subject: [PATCH 03/11] Move jruby to allow failures matrix till crc uint 32 issues are resolved --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ad59d61e..b197c86b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ matrix: - rvm: ruby-head - rvm: rbx-3 - rvm: jruby-head + - rvm: jruby before_install: - gem update --system - gem install bundler From 0586329d3be19728c20941faa401cb838f461dc3 Mon Sep 17 00:00:00 2001 From: Bart de Water Date: Sun, 26 Aug 2018 00:52:10 -0400 Subject: [PATCH 04/11] Trigger CI again From 9c468f30f38d09451e5a65edfff277cfe381fd49 Mon Sep 17 00:00:00 2001 From: John Lees-Miller Date: Sun, 26 Aug 2018 10:00:35 +0100 Subject: [PATCH 05/11] Add jwilk's path traversal tests --- .../jwilk-path-traversal-samples/README.md | 5 + .../absolute1.zip | Bin 0 -> 118 bytes .../absolute2.zip | Bin 0 -> 120 bytes .../dirsymlink.zip | Bin 0 -> 202 bytes .../dirsymlink2a.zip | Bin 0 -> 287 bytes .../dirsymlink2b.zip | Bin 0 -> 291 bytes .../relative0.zip | Bin 0 -> 114 bytes .../relative2.zip | Bin 0 -> 128 bytes .../jwilk-path-traversal-samples/symlink.zip | Bin 0 -> 198 bytes test/path_traversal_test.rb | 88 ++++++++++++++++++ 10 files changed, 93 insertions(+) create mode 100644 test/data/jwilk-path-traversal-samples/README.md create mode 100644 test/data/jwilk-path-traversal-samples/absolute1.zip create mode 100644 test/data/jwilk-path-traversal-samples/absolute2.zip create mode 100644 test/data/jwilk-path-traversal-samples/dirsymlink.zip create mode 100644 test/data/jwilk-path-traversal-samples/dirsymlink2a.zip create mode 100644 test/data/jwilk-path-traversal-samples/dirsymlink2b.zip create mode 100644 test/data/jwilk-path-traversal-samples/relative0.zip create mode 100644 test/data/jwilk-path-traversal-samples/relative2.zip create mode 100644 test/data/jwilk-path-traversal-samples/symlink.zip create mode 100644 test/path_traversal_test.rb diff --git a/test/data/jwilk-path-traversal-samples/README.md b/test/data/jwilk-path-traversal-samples/README.md new file mode 100644 index 00000000..2ecceb23 --- /dev/null +++ b/test/data/jwilk-path-traversal-samples/README.md @@ -0,0 +1,5 @@ +# Path Traversal Samples + +Copied from https://github.com/jwilk/path-traversal-samples on 2018-08-26. + +License: MIT diff --git a/test/data/jwilk-path-traversal-samples/absolute1.zip b/test/data/jwilk-path-traversal-samples/absolute1.zip new file mode 100644 index 0000000000000000000000000000000000000000..27c615d98bae9a331eedd1425f5830c40812fd2c GIT binary patch literal 118 zcmWIWW@h1H0D<3gj{B^9WW16E$Od5!Al5I*Ezr-+&j%u|0B=SnIcD5yfyx;efp|$H Why~Lb;LXYg;xhuF8IaZjaTox`+Z8VW literal 0 HcmV?d00001 diff --git a/test/data/jwilk-path-traversal-samples/absolute2.zip b/test/data/jwilk-path-traversal-samples/absolute2.zip new file mode 100644 index 0000000000000000000000000000000000000000..c82c14eaa68624f85dd0f8ed63da5cef56c0bb24 GIT binary patch literal 120 zcmWIWW@h1H0D<3gj{B^9WW16E$Od6fAlBC}$t}>&&CdrSt^jXFCOKx@ih=4G7=d_6 YBZvjp8sN>!1`=QdLUSOk4dO5W0Q9UCkpKVy literal 0 HcmV?d00001 diff --git a/test/data/jwilk-path-traversal-samples/dirsymlink.zip b/test/data/jwilk-path-traversal-samples/dirsymlink.zip new file mode 100644 index 0000000000000000000000000000000000000000..978b5d8a061316af9e1d7de091032281dee5e515 GIT binary patch literal 202 zcmWIWW@h1H0D<3gj{E2x+0DfQWP>m>5SQc@=mT*8ilUW|j90=Gu|pN*=H~+uSAaJo zlN>W{^MI3K_6&MfHx}}NP-Cn(}1)eh{FH?U=Sr) literal 0 HcmV?d00001 diff --git a/test/data/jwilk-path-traversal-samples/dirsymlink2a.zip b/test/data/jwilk-path-traversal-samples/dirsymlink2a.zip new file mode 100644 index 0000000000000000000000000000000000000000..443deede33be9e5eaf90b496296becca2e181d52 GIT binary patch literal 287 zcmWIWW@h1H0D<3gj{7)0y28f@WP>m>5GR)w=>?#uICw8jn++%f!XOm|iA6v~`g&-} zRz5Ob$pVxHVRncz{oMR~AmR$}W@M6M#%(81H5mL~2%><7qiaBS0z^3j!~ca!Kqk;Z k=o-)+1JVHU;gUvAhz5|;0=!v4egb-mnPEDRE&_2F0PO@eP5=M^ literal 0 HcmV?d00001 diff --git a/test/data/jwilk-path-traversal-samples/dirsymlink2b.zip b/test/data/jwilk-path-traversal-samples/dirsymlink2b.zip new file mode 100644 index 0000000000000000000000000000000000000000..5a5a12b4b6bb08eb331850e1a2547fb78434d1a2 GIT binary patch literal 291 zcmWIWW@h1H0D<3gj{7)0y28f@WP>m>5GR)w=>?#ukP+jU#sriBVRncL{er|IJv3!2 z9~rM?0ZM}~NEuLxer|p~5OD=~Gcw6BJ85n_hNh62_ V(HG#&3Ni#J&d6W_q%}Ys1^^b>65jv- literal 0 HcmV?d00001 diff --git a/test/data/jwilk-path-traversal-samples/relative2.zip b/test/data/jwilk-path-traversal-samples/relative2.zip new file mode 100644 index 0000000000000000000000000000000000000000..8957028d9597f9658eae2df18f0e138ead49714f GIT binary patch literal 128 zcmWIWW@h1H0D<3gj{B^9WW16E$Od6vATG%*(AU!gq1^m@AmR$}W@M6M#;pOUhk+4@ Zmo$P{DB1(OS=m5>j6i4&r1e1@1^~L>7ZCse literal 0 HcmV?d00001 diff --git a/test/data/jwilk-path-traversal-samples/symlink.zip b/test/data/jwilk-path-traversal-samples/symlink.zip new file mode 100644 index 0000000000000000000000000000000000000000..edaa7526aeb60c51a2f8140ee9b34767964a7cc4 GIT binary patch literal 198 zcmWIWW@h1H0D<3gj{8J;-<{3@WP>m>5a;IS>zCvf=mY5h6oo4v8LwmkDga@qLZBM1 x0B=SnIcD7E0ab&+|Ain5Y$~b-baO$<7=d_6qb5uPD;r3V2?&#bv>%AW001u9B$EID literal 0 HcmV?d00001 diff --git a/test/path_traversal_test.rb b/test/path_traversal_test.rb new file mode 100644 index 00000000..ab8269b7 --- /dev/null +++ b/test/path_traversal_test.rb @@ -0,0 +1,88 @@ +class PathTraversalTest < MiniTest::Test + TEST_FILE_ROOT = File.absolute_path('test/data/jwilk-path-traversal-samples') + + def setup + FileUtils.rm_f '/tmp/moo' # with apologies to anyone using this file + end + + def extract_path_traversal_zip(name) + Zip::File.open(File.join(TEST_FILE_ROOT, name)) do |zip_file| + zip_file.each do |entry| + entry.extract + end + end + end + + def in_tmpdir + Dir.mktmpdir do |tmp| + test_path = File.join(tmp, 'test') + Dir.mkdir test_path + Dir.chdir(test_path) do + yield + end + end + end + + def test_leading_slash + in_tmpdir do + extract_path_traversal_zip 'absolute1.zip' + assert !File.exist?('/tmp/moo') + end + end + + def test_multiple_leading_slashes + in_tmpdir do + extract_path_traversal_zip 'absolute2.zip' + assert !File.exist?('/tmp/moo') + end + end + + def test_leading_dot_dot + in_tmpdir do + extract_path_traversal_zip 'relative0.zip' + assert !File.exist?('../moo') + end + end + + def test_non_leading_dot_dot + in_tmpdir do + extract_path_traversal_zip 'relative2.zip' + assert !File.exist?('../moo') + end + end + + def test_file_symlink + in_tmpdir do + extract_path_traversal_zip 'symlink.zip' + assert File.exist?('moo') + assert !File.exist?('/tmp/moo') + end + end + + def test_directory_symlink + in_tmpdir do + extract_path_traversal_zip 'dirsymlink.zip' + assert !File.exist?('/tmp/moo') + end + end + + def test_two_directory_symlinks_a + in_tmpdir do + # Can't create par/moo because the symlink par is skipped. + assert_raises Errno::ENOENT do + extract_path_traversal_zip 'dirsymlink2a.zip' + end + assert File.exist?('cur') + assert_equal '.', File.readlink('cur') + end + end + + def test_two_directory_symlinks_b + in_tmpdir do + extract_path_traversal_zip 'dirsymlink2b.zip' + assert File.exist?('cur') + assert_equal '.', File.readlink('cur') + assert !File.exist?('../moo') + end + end +end From ffebfa34189a46a766bf6630796c93d81b5ef7ed Mon Sep 17 00:00:00 2001 From: John Lees-Miller Date: Sun, 26 Aug 2018 12:13:12 +0100 Subject: [PATCH 06/11] Consolidate path traversal tests --- .../jwilk}/README.md | 0 .../jwilk}/absolute1.zip | Bin .../jwilk}/absolute2.zip | Bin .../jwilk}/dirsymlink.zip | Bin .../jwilk}/dirsymlink2a.zip | Bin .../jwilk}/dirsymlink2b.zip | Bin .../jwilk}/relative0.zip | Bin .../jwilk}/relative2.zip | Bin .../jwilk}/symlink.zip | Bin .../data/path_traversal/tuzovakaoff/README.md | 3 + .../tuzovakaoff}/absolutepath.zip | Bin .../tuzovakaoff}/symlink.zip | Bin test/entry_test.rb | 36 -------- test/path_traversal_test.rb | 77 +++++++++++++----- 14 files changed, 58 insertions(+), 58 deletions(-) rename test/data/{jwilk-path-traversal-samples => path_traversal/jwilk}/README.md (100%) rename test/data/{jwilk-path-traversal-samples => path_traversal/jwilk}/absolute1.zip (100%) rename test/data/{jwilk-path-traversal-samples => path_traversal/jwilk}/absolute2.zip (100%) rename test/data/{jwilk-path-traversal-samples => path_traversal/jwilk}/dirsymlink.zip (100%) rename test/data/{jwilk-path-traversal-samples => path_traversal/jwilk}/dirsymlink2a.zip (100%) rename test/data/{jwilk-path-traversal-samples => path_traversal/jwilk}/dirsymlink2b.zip (100%) rename test/data/{jwilk-path-traversal-samples => path_traversal/jwilk}/relative0.zip (100%) rename test/data/{jwilk-path-traversal-samples => path_traversal/jwilk}/relative2.zip (100%) rename test/data/{jwilk-path-traversal-samples => path_traversal/jwilk}/symlink.zip (100%) create mode 100644 test/data/path_traversal/tuzovakaoff/README.md rename test/data/{ => path_traversal/tuzovakaoff}/absolutepath.zip (100%) rename test/data/{ => path_traversal/tuzovakaoff}/symlink.zip (100%) diff --git a/test/data/jwilk-path-traversal-samples/README.md b/test/data/path_traversal/jwilk/README.md similarity index 100% rename from test/data/jwilk-path-traversal-samples/README.md rename to test/data/path_traversal/jwilk/README.md diff --git a/test/data/jwilk-path-traversal-samples/absolute1.zip b/test/data/path_traversal/jwilk/absolute1.zip similarity index 100% rename from test/data/jwilk-path-traversal-samples/absolute1.zip rename to test/data/path_traversal/jwilk/absolute1.zip diff --git a/test/data/jwilk-path-traversal-samples/absolute2.zip b/test/data/path_traversal/jwilk/absolute2.zip similarity index 100% rename from test/data/jwilk-path-traversal-samples/absolute2.zip rename to test/data/path_traversal/jwilk/absolute2.zip diff --git a/test/data/jwilk-path-traversal-samples/dirsymlink.zip b/test/data/path_traversal/jwilk/dirsymlink.zip similarity index 100% rename from test/data/jwilk-path-traversal-samples/dirsymlink.zip rename to test/data/path_traversal/jwilk/dirsymlink.zip diff --git a/test/data/jwilk-path-traversal-samples/dirsymlink2a.zip b/test/data/path_traversal/jwilk/dirsymlink2a.zip similarity index 100% rename from test/data/jwilk-path-traversal-samples/dirsymlink2a.zip rename to test/data/path_traversal/jwilk/dirsymlink2a.zip diff --git a/test/data/jwilk-path-traversal-samples/dirsymlink2b.zip b/test/data/path_traversal/jwilk/dirsymlink2b.zip similarity index 100% rename from test/data/jwilk-path-traversal-samples/dirsymlink2b.zip rename to test/data/path_traversal/jwilk/dirsymlink2b.zip diff --git a/test/data/jwilk-path-traversal-samples/relative0.zip b/test/data/path_traversal/jwilk/relative0.zip similarity index 100% rename from test/data/jwilk-path-traversal-samples/relative0.zip rename to test/data/path_traversal/jwilk/relative0.zip diff --git a/test/data/jwilk-path-traversal-samples/relative2.zip b/test/data/path_traversal/jwilk/relative2.zip similarity index 100% rename from test/data/jwilk-path-traversal-samples/relative2.zip rename to test/data/path_traversal/jwilk/relative2.zip diff --git a/test/data/jwilk-path-traversal-samples/symlink.zip b/test/data/path_traversal/jwilk/symlink.zip similarity index 100% rename from test/data/jwilk-path-traversal-samples/symlink.zip rename to test/data/path_traversal/jwilk/symlink.zip diff --git a/test/data/path_traversal/tuzovakaoff/README.md b/test/data/path_traversal/tuzovakaoff/README.md new file mode 100644 index 00000000..4bfd8cb6 --- /dev/null +++ b/test/data/path_traversal/tuzovakaoff/README.md @@ -0,0 +1,3 @@ +# Path Traversal Samples + +Copied from https://github.com/jwilk/path-traversal-samples on 2018-08-25. diff --git a/test/data/absolutepath.zip b/test/data/path_traversal/tuzovakaoff/absolutepath.zip similarity index 100% rename from test/data/absolutepath.zip rename to test/data/path_traversal/tuzovakaoff/absolutepath.zip diff --git a/test/data/symlink.zip b/test/data/path_traversal/tuzovakaoff/symlink.zip similarity index 100% rename from test/data/symlink.zip rename to test/data/path_traversal/tuzovakaoff/symlink.zip diff --git a/test/entry_test.rb b/test/entry_test.rb index eaa9c0d9..b49783d3 100644 --- a/test/entry_test.rb +++ b/test/entry_test.rb @@ -151,40 +151,4 @@ def test_store_file_without_compression assert_match(/mimetypeapplication\/epub\+zip/, first_100_bytes) end - - def test_entry_name_with_absolute_path_does_not_extract - path = '/tmp/file.txt' - File.delete(path) if File.exist?(path) - - Zip::File.open('test/data/absolutepath.zip') do |zip_file| - zip_file.each do |entry| - entry.extract - end - end - - refute File.exist?(path) - end - - def test_entry_name_with_absolute_path_extract_when_given_different_path - path = '/tmp/CVE-2018-1000544' - FileUtils.rm_rf(path) if Dir.exist?(path) - - Zip::File.open('test/data/absolutepath.zip') do |zip_file| - zip_file.each do |entry| - entry.extract("#{path}/#{entry.name}") - end - end - - assert File.exist?("#{path}/tmp/file.txt") - end - - def test_entry_name_with_relative_symlink - assert_raises Errno::ENOENT do - Zip::File.open('test/data/symlink.zip') do |zip_file| - zip_file.each do |entry| - entry.extract - end - end - end - end end diff --git a/test/path_traversal_test.rb b/test/path_traversal_test.rb index ab8269b7..406fc0ef 100644 --- a/test/path_traversal_test.rb +++ b/test/path_traversal_test.rb @@ -1,8 +1,11 @@ class PathTraversalTest < MiniTest::Test - TEST_FILE_ROOT = File.absolute_path('test/data/jwilk-path-traversal-samples') + TEST_FILE_ROOT = File.absolute_path('test/data/path_traversal') def setup - FileUtils.rm_f '/tmp/moo' # with apologies to anyone using this file + # With apologies to anyone using these files... but they are the files in + # the sample zips, so we don't have much choice here. + FileUtils.rm_f '/tmp/moo' + FileUtils.rm_f '/tmp/file.txt' end def extract_path_traversal_zip(name) @@ -17,72 +20,102 @@ def in_tmpdir Dir.mktmpdir do |tmp| test_path = File.join(tmp, 'test') Dir.mkdir test_path - Dir.chdir(test_path) do - yield + Dir.chdir test_path do + yield test_path end end end def test_leading_slash in_tmpdir do - extract_path_traversal_zip 'absolute1.zip' - assert !File.exist?('/tmp/moo') + extract_path_traversal_zip 'jwilk/absolute1.zip' + refute File.exist?('/tmp/moo') end end def test_multiple_leading_slashes in_tmpdir do - extract_path_traversal_zip 'absolute2.zip' - assert !File.exist?('/tmp/moo') + extract_path_traversal_zip 'jwilk/absolute2.zip' + refute File.exist?('/tmp/moo') end end def test_leading_dot_dot in_tmpdir do - extract_path_traversal_zip 'relative0.zip' - assert !File.exist?('../moo') + extract_path_traversal_zip 'jwilk/relative0.zip' + refute File.exist?('../moo') end end def test_non_leading_dot_dot in_tmpdir do - extract_path_traversal_zip 'relative2.zip' - assert !File.exist?('../moo') + extract_path_traversal_zip 'jwilk/relative2.zip' + refute File.exist?('../moo') end end def test_file_symlink in_tmpdir do - extract_path_traversal_zip 'symlink.zip' + extract_path_traversal_zip 'jwilk/symlink.zip' assert File.exist?('moo') - assert !File.exist?('/tmp/moo') + refute File.exist?('/tmp/moo') end end def test_directory_symlink in_tmpdir do - extract_path_traversal_zip 'dirsymlink.zip' - assert !File.exist?('/tmp/moo') + extract_path_traversal_zip 'jwilk/dirsymlink.zip' + refute File.exist?('/tmp/moo') end end def test_two_directory_symlinks_a in_tmpdir do - # Can't create par/moo because the symlink par is skipped. + # Can't create par/moo because the symlinks are skipped. assert_raises Errno::ENOENT do - extract_path_traversal_zip 'dirsymlink2a.zip' + extract_path_traversal_zip 'jwilk/dirsymlink2a.zip' end - assert File.exist?('cur') - assert_equal '.', File.readlink('cur') + refute File.exist?('cur') + refute File.exist?('par') + refute File.exist?('par/moo') end end def test_two_directory_symlinks_b in_tmpdir do - extract_path_traversal_zip 'dirsymlink2b.zip' + extract_path_traversal_zip 'jwilk/dirsymlink2b.zip' assert File.exist?('cur') assert_equal '.', File.readlink('cur') - assert !File.exist?('../moo') + refute File.exist?('../moo') + end + end + + def test_entry_name_with_absolute_path_does_not_extract + in_tmpdir do + extract_path_traversal_zip 'tuzovakaoff/absolutepath.zip' + refute File.exist?('/tmp/file.txt') + end + end + + def test_entry_name_with_absolute_path_extract_when_given_different_path + in_tmpdir do |test_path| + zip_path = File.join(TEST_FILE_ROOT, 'tuzovakaoff/absolutepath.zip') + Zip::File.open(zip_path) do |zip_file| + zip_file.each do |entry| + entry.extract(File.join(test_path, entry.name)) + end + end + refute File.exist?('/tmp/file.txt') + end + end + + def test_entry_name_with_relative_symlink + in_tmpdir do + # Doesn't create the symlink path, so can't create path/file.txt. + assert_raises Errno::ENOENT do + extract_path_traversal_zip 'tuzovakaoff/symlink.zip' + end + refute File.exist?('/tmp/file.txt') end end end From 3dd165b494f29d410184b2a135ed99527d4b4aa8 Mon Sep 17 00:00:00 2001 From: John Lees-Miller Date: Sun, 26 Aug 2018 12:32:18 +0100 Subject: [PATCH 07/11] Disable symlinks and check for path traversal --- lib/zip/entry.rb | 51 ++++++------------ test/data/path_traversal/Makefile | 10 ++++ test/data/path_traversal/relative1.zip | Bin 0 -> 212 bytes .../data/path_traversal/tuzovakaoff/README.md | 2 +- test/data/rubycode.zip | Bin 617 -> 617 bytes test/path_traversal_test.rb | 23 ++++++-- 6 files changed, 46 insertions(+), 40 deletions(-) create mode 100644 test/data/path_traversal/Makefile create mode 100644 test/data/path_traversal/relative1.zip diff --git a/lib/zip/entry.rb b/lib/zip/entry.rb index 28d60091..34317d43 100644 --- a/lib/zip/entry.rb +++ b/lib/zip/entry.rb @@ -109,6 +109,16 @@ def name_is_directory? #:nodoc:all @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 @@ def next_header_offset #:nodoc:all 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 @@ def create_directory(dest_path) # 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 diff --git a/test/data/path_traversal/Makefile b/test/data/path_traversal/Makefile new file mode 100644 index 00000000..9ff4d816 --- /dev/null +++ b/test/data/path_traversal/Makefile @@ -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 diff --git a/test/data/path_traversal/relative1.zip b/test/data/path_traversal/relative1.zip new file mode 100644 index 0000000000000000000000000000000000000000..bfcb9deff18c2655f2bcf109d09967a37a9ccb11 GIT binary patch literal 212 zcmWIWW@h1H0D-whQodjYlwbkUCAkIq0Vpa~J~Cbjk^x~}s0uwjeIUxs&j%u|0B=Sn zIcD5O0`&la0K;2H5Dl>aRR_8iAa#sDyrfYHssnCefHx}}NSX-nD;YsN0Ky3(ga7~l delta 151 zcmaFK@{(nOu1E+c1M{&>2W-x6KVTDD!NQQLEvPxsu?j4H5JOyj; Date: Sun, 26 Aug 2018 19:55:26 +0100 Subject: [PATCH 08/11] Expand from root rather than current working directory --- lib/zip/entry.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/zip/entry.rb b/lib/zip/entry.rb index 34317d43..fddab51e 100644 --- a/lib/zip/entry.rb +++ b/lib/zip/entry.rb @@ -115,8 +115,9 @@ def name_is_directory? #:nodoc:all 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 + root = ::File::SEPARATOR + naive_expanded_path = ::File.join(root, cleanpath.to_s) + cleanpath.expand_path(root).to_s == naive_expanded_path end def local_entry_offset #:nodoc:all From ffb374c6b1757f6b5eb93e68b8b37ebc7df3f310 Mon Sep 17 00:00:00 2001 From: John Lees-Miller Date: Mon, 27 Aug 2018 08:37:53 +0100 Subject: [PATCH 09/11] Bump version to 2.0.0 --- lib/zip/version.rb | 2 +- test/data/rubycode.zip | Bin 617 -> 617 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/zip/version.rb b/lib/zip/version.rb index f1dc85f4..eb9cfa9b 100644 --- a/lib/zip/version.rb +++ b/lib/zip/version.rb @@ -1,3 +1,3 @@ module Zip - VERSION = '1.2.1' + VERSION = '2.0.0' end diff --git a/test/data/rubycode.zip b/test/data/rubycode.zip index 06134bbcdb260c14da8d915eebdf2ca7176ac723..7f923a08cf83596d9cdc5f6d61ae6809983f7816 100644 GIT binary patch delta 139 zcmaFK@{(nOu0RMU1M{&>2W%LCAhd#oVWMpvSnME*nD)eP!C>J{+Yw49M>59pgs?*N shgL8!Og_YD3l;!r1`BXA*(iZjfV6=G0=!w-KsGT0;VmFt$q3>B0BR>9cmMzZ delta 139 zcmaFK@{(nOu0RC~0|$E_QY?&D8iezAF!Dm$r#I10n*C~ m(L4DNqb*zjq;WDglZ_Hg1xQPPH!B;+I%Xie1*9t(K|BC1`674# From cf35774ed686057d8cc17aa4b015a2a850cc2bce Mon Sep 17 00:00:00 2001 From: John Lees-Miller Date: Mon, 27 Aug 2018 09:02:11 +0100 Subject: [PATCH 10/11] Bump version to 1.3.0 --- lib/zip/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/zip/version.rb b/lib/zip/version.rb index eb9cfa9b..37fba090 100644 --- a/lib/zip/version.rb +++ b/lib/zip/version.rb @@ -1,3 +1,3 @@ module Zip - VERSION = '2.0.0' + VERSION = '1.3.0' end From fd81bd523cd53096c1a1dce1e950ef0b7658a02c Mon Sep 17 00:00:00 2001 From: John Lees-Miller Date: Mon, 27 Aug 2018 09:07:21 +0100 Subject: [PATCH 11/11] Bump version to 1.2.2 --- lib/zip/version.rb | 2 +- test/data/rubycode.zip | Bin 617 -> 617 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/zip/version.rb b/lib/zip/version.rb index 37fba090..14a9f99e 100644 --- a/lib/zip/version.rb +++ b/lib/zip/version.rb @@ -1,3 +1,3 @@ module Zip - VERSION = '1.3.0' + VERSION = '1.2.2' end diff --git a/test/data/rubycode.zip b/test/data/rubycode.zip index 7f923a08cf83596d9cdc5f6d61ae6809983f7816..06134bbcdb260c14da8d915eebdf2ca7176ac723 100644 GIT binary patch delta 139 zcmaFK@{(nOu0RC~0|$E_QY?&D8iezAF!Dm$r#I10n*C~ m(L4DNqb*zjq;WDglZ_Hg1xQPPH!B;+I%Xie1*9t(K|BC1`674# delta 139 zcmaFK@{(nOu0RMU1M{&>2W%LCAhd#oVWMpvSnME*nD)eP!C>J{+Yw49M>59pgs?*N shgL8!Og_YD3l;!r1`BXA*(iZjfV6=G0=!w-KsGT0;VmFt$q3>B0BR>9cmMzZ