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