1# frozen_string_literal: true 2 3require 'spec_helper' 4 5RSpec.describe SafeZip::Entry do 6 let(:target_path) { Dir.mktmpdir('safe-zip') } 7 let(:directories) { %w(public folder/with/subfolder) } 8 let(:params) { SafeZip::ExtractParams.new(directories: directories, to: target_path) } 9 10 let(:entry) { described_class.new(zip_archive, zip_entry, params) } 11 let(:entry_name) { 'public/folder/index.html' } 12 let(:entry_path_dir) { File.join(target_path, File.dirname(entry_name)) } 13 let(:entry_path) { File.join(target_path, entry_name) } 14 let(:zip_archive) { double } 15 16 let(:zip_entry) do 17 double( 18 name: entry_name, 19 file?: false, 20 directory?: false, 21 symlink?: false) 22 end 23 24 after do 25 FileUtils.remove_entry_secure(target_path) 26 end 27 28 describe '#path_dir' do 29 subject { entry.path_dir } 30 31 it { is_expected.to eq(target_path + '/public/folder') } 32 end 33 34 describe '#exist?' do 35 subject { entry.exist? } 36 37 context 'when entry does not exist' do 38 it { is_expected.not_to be_truthy } 39 end 40 41 context 'when entry does exist' do 42 before do 43 create_entry 44 end 45 46 it { is_expected.to be_truthy } 47 end 48 end 49 50 describe '#extract' do 51 subject { entry.extract } 52 53 context 'when entry does not match the filtered directories' do 54 using RSpec::Parameterized::TableSyntax 55 56 where(:entry_name) do 57 [ 58 'assets/folder/index.html', 59 'public/../folder/index.html', 60 'public/../../../../../index.html', 61 '../../../../../public/index.html', 62 '/etc/passwd' 63 ] 64 end 65 66 with_them do 67 it 'does not extract file' do 68 is_expected.to be_falsey 69 end 70 end 71 end 72 73 context 'when entry does exist' do 74 before do 75 create_entry 76 end 77 78 it 'raises an exception' do 79 expect { subject }.to raise_error(SafeZip::Extract::AlreadyExistsError) 80 end 81 end 82 83 context 'when entry type is unknown' do 84 it 'raises an exception' do 85 expect { subject }.to raise_error(SafeZip::Extract::UnsupportedEntryError) 86 end 87 end 88 89 context 'when entry is valid' do 90 shared_examples 'secured symlinks' do 91 context 'when we try to extract entry into symlinked folder' do 92 before do 93 FileUtils.mkdir_p(File.join(target_path, "source")) 94 File.symlink("source", File.join(target_path, "public")) 95 end 96 97 it 'raises an exception' do 98 expect { subject }.to raise_error(SafeZip::Extract::PermissionDeniedError) 99 end 100 end 101 end 102 103 context 'and is file' do 104 before do 105 allow(zip_entry).to receive(:file?) { true } 106 end 107 108 it 'does extract file' do 109 expect(zip_archive).to receive(:extract) 110 .with(zip_entry, entry_path) 111 .and_return(true) 112 113 is_expected.to be_truthy 114 end 115 116 it_behaves_like 'secured symlinks' 117 end 118 119 context 'and is directory' do 120 let(:entry_name) { 'public/folder/assets' } 121 122 before do 123 allow(zip_entry).to receive(:directory?) { true } 124 end 125 126 it 'does create directory' do 127 is_expected.to be_truthy 128 129 expect(File.exist?(entry_path)).to eq(true) 130 end 131 132 it_behaves_like 'secured symlinks' 133 end 134 135 context 'and is symlink' do 136 let(:entry_name) { 'public/folder/assets' } 137 138 before do 139 allow(zip_entry).to receive(:symlink?) { true } 140 allow(zip_archive).to receive(:read).with(zip_entry) { entry_symlink } 141 end 142 143 shared_examples 'a valid symlink' do 144 it 'does create symlink' do 145 is_expected.to be_truthy 146 147 expect(File.exist?(entry_path)).to eq(true) 148 end 149 end 150 151 context 'when source is within target' do 152 let(:entry_symlink) { '../images' } 153 154 context 'but does not exist' do 155 it 'raises an exception' do 156 expect { subject }.to raise_error(SafeZip::Extract::SymlinkSourceDoesNotExistError) 157 end 158 end 159 160 context 'and does exist' do 161 before do 162 FileUtils.mkdir_p(File.join(target_path, 'public', 'images')) 163 end 164 165 it_behaves_like 'a valid symlink' 166 end 167 end 168 169 context 'when source points outside of target' do 170 let(:entry_symlink) { '../../images' } 171 172 before do 173 FileUtils.mkdir(File.join(target_path, 'images')) 174 end 175 176 it 'raises an exception' do 177 expect { subject }.to raise_error(SafeZip::Extract::PermissionDeniedError) 178 end 179 end 180 181 context 'when source points to /etc/passwd' do 182 let(:entry_symlink) { '/etc/passwd' } 183 184 it 'raises an exception' do 185 expect { subject }.to raise_error(SafeZip::Extract::PermissionDeniedError) 186 end 187 end 188 end 189 end 190 end 191 192 private 193 194 def create_entry 195 FileUtils.mkdir_p(entry_path_dir) 196 FileUtils.touch(entry_path) 197 end 198end 199