Skip to content
This repository was archived by the owner on Jun 2, 2021. It is now read-only.

Commit a11dc99

Browse files
committed
Prevent files with insufficient permissions from being cached
- Previously, we were altering file permissions when we unzipped the resources for an app to give ourselves read/write access. The altered files were then stored in the resource cache - When attempting to push the app containing a file with insufficient permissions for a second time, a resource match would be found. We preserve the mode provided in the fingerprint given to us from the client, rather than using the altered mode the file was cached with. (endpoint: POST /v3/resource_matches) - When the client reached the upload bits portion of push, it would pass along the fingerprints matched in the step above. The request would then error out, as it would run into a check we have to ensure that the mode on all fingerprints is >= 0600. (endpoint: POST /v3/packages/:guid/upload) To solve this issue we have done the following: - Moved the portion of code that altered permissions on unzipping the application. This is still necessary in allowing CAPI to clean up the unzipped files, and is now done as part of `remove_dirs_from_zip`. If changing the permissions fails, an error is logged but it does not cause the call to fail. - Enforced a file permission check before uploading any files to the resource cache. This prevents files with permissions < 0600 from being uploaded in the first place, ultimately preventing a resource match from ever being returned for insufficiently permissioned files. Users who have previously uploaded an insufficiently permissioned file may need to clear their resource cache as these changes do not account for any files already in the cache. Github issue: cloudfoundry#1705 [finishes #173541786] Authored-by: Sarah Weinstein <sweinstein@pivotal.io>
1 parent 95f7465 commit a11dc99

File tree

7 files changed

+89
-95
lines changed

7 files changed

+89
-95
lines changed

lib/cloud_controller/app_packager.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ def unzip(destination_dir)
2525
logger.error("Unzipping had errors\n STDOUT: \"#{output}\"\n STDERR: \"#{error}\"")
2626
invalid_zip!
2727
end
28-
29-
fix_unzipped_permissions(destination_dir)
3028
end
3129

3230
def append_dir_contents(additional_contents_dir)
@@ -43,8 +41,8 @@ def append_dir_contents(additional_contents_dir)
4341
end
4442
end
4543

46-
def fix_subdir_permissions
47-
remove_dirs_from_zip(@path, get_dirs_from_zip(@path))
44+
def fix_subdir_permissions(root_path, app_contents_path)
45+
remove_dirs_from_zip(@path, get_dirs_from_zip(@path), root_path, app_contents_path)
4846
rescue Zip::Error
4947
invalid_zip!
5048
end
@@ -69,7 +67,10 @@ def logger
6967
@logger ||= Steno.logger('app_packager')
7068
end
7169

72-
def remove_dirs_from_zip(zip_path, dirs_from_zip)
70+
def remove_dirs_from_zip(zip_path, dirs_from_zip, root_path, app_contents_path)
71+
unless empty_directory?(root_path)
72+
fix_permissions_for_file_deletion(app_contents_path)
73+
end
7374
dirs_from_zip.each_slice(DIRECTORY_DELETE_BATCH_SIZE) do |directory_slice|
7475
remove_dir(zip_path, directory_slice)
7576
end
@@ -87,11 +88,10 @@ def remove_dir(zip_path, directories)
8788
end
8889
end
8990

90-
def fix_unzipped_permissions(destination_dir)
91+
def fix_permissions_for_file_deletion(destination_dir)
9192
stdout, error, status = Open3.capture3(%(chmod -R u+rwX #{Shellwords.escape(destination_dir)}))
9293
unless status.success?
93-
logger.error("Fixing zip file permissions error\n STDOUT: \"#{stdout}\"\n STDERR: \"#{error}\"")
94-
invalid_zip!
94+
logger.error("Cleanup of some files may have failed, error fixing zip file permissions\n STDOUT: \"#{stdout}\"\n STDERR: \"#{error}\"")
9595
end
9696
end
9797

lib/cloud_controller/blobstore/base_client.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ def cp_r_to_blobstore(source_dir)
88
Find.find(source_dir).each do |path|
99
next unless File.file?(path)
1010
next unless within_limits?(File.size(path))
11+
next unless File.stat(path).mode.to_s(8)[3..5].to_i(8) >= 0600
1112

1213
sha1 = Digester.new.digest_path(path)
1314
next if exists?(sha1)

lib/cloud_controller/packager/shared_bits_packer.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ def copy_uploaded_package(uploaded_package_zip, app_packager)
2222
FileUtils.cp(uploaded_package_zip, app_packager.path)
2323
end
2424

25-
def populate_resource_cache(app_packager, root_path)
26-
app_contents_path = File.join(root_path, 'application_contents')
25+
def populate_resource_cache(app_packager, app_contents_path)
2726
FileUtils.mkdir(app_contents_path)
2827
app_packager.unzip(app_contents_path)
2928

@@ -51,17 +50,18 @@ def append_matched_resources(app_packager, cached_files_fingerprints, root_path)
5150

5251
def match_resources_and_validate_package(root_path, uploaded_package_zip, cached_files_fingerprints)
5352
app_packager = AppPackager.new(File.join(root_path, 'copied_app_package.zip'))
53+
app_contents_path = File.join(root_path, 'application_contents')
5454

5555
if package_zip_exists?(uploaded_package_zip)
5656
copy_uploaded_package(uploaded_package_zip, app_packager)
5757
validate_size!(app_packager)
58-
populate_resource_cache(app_packager, root_path)
58+
populate_resource_cache(app_packager, app_contents_path)
5959
end
6060

6161
append_matched_resources(app_packager, cached_files_fingerprints, root_path)
6262

6363
validate_size!(app_packager)
64-
app_packager.fix_subdir_permissions
64+
app_packager.fix_subdir_permissions(root_path, app_contents_path)
6565
app_packager.path
6666
end
6767

spec/unit/lib/app_packager_spec.rb

Lines changed: 7 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -32,39 +32,6 @@
3232
expect(Dir["#{@tmpdir}/subdir/*"].size).to eq 1
3333
end
3434

35-
context 'when the zip contains files with weird permissions' do
36-
context 'when there are unreadable dirs' do
37-
let(:input_zip) { File.join(Paths::FIXTURES, 'app_packager_zips', 'unreadable_dir.zip') }
38-
39-
it 'makes all files/dirs readable to cc' do
40-
app_packager.unzip(@tmpdir)
41-
42-
expect(File.readable?("#{@tmpdir}/unreadable")).to be true
43-
end
44-
end
45-
46-
context 'when there are unwritable dirs' do
47-
let(:input_zip) { File.join(Paths::FIXTURES, 'app_packager_zips', 'undeletable_dir.zip') }
48-
49-
it 'makes all files/dirs writable to cc' do
50-
app_packager.unzip(@tmpdir)
51-
52-
expect(File.writable?("#{@tmpdir}/undeletable")).to be true
53-
end
54-
end
55-
56-
context 'when there are untraversable dirs' do
57-
let(:input_zip) { File.join(Paths::FIXTURES, 'app_packager_zips', 'untraversable_dir.zip') }
58-
59-
it 'makes all dirs traversable to cc' do
60-
app_packager.unzip(@tmpdir)
61-
62-
expect(File.executable?("#{@tmpdir}/untraversable")).to be true
63-
expect(File.executable?("#{@tmpdir}/untraversable/file.txt")).to be false
64-
end
65-
end
66-
end
67-
6835
context 'when the zip contains broken symlinks' do
6936
let(:input_zip) { File.join(Paths::FIXTURES, 'app_packager_zips', 'broken-file-symlink.zip') }
7037

@@ -126,21 +93,6 @@
12693
}.to raise_error(CloudController::Errors::ApiError, /Invalid zip archive/)
12794
end
12895
end
129-
130-
context 'when there is an error adjusting permissions' do
131-
before do
132-
allow(Open3).to receive(:capture3).with(/unzip/).and_return(['output', 'error', double(success?: true)])
133-
allow(Open3).to receive(:capture3).with(/chmod/).and_return(['output', 'error', double(success?: false)])
134-
end
135-
136-
it 'raises an exception' do
137-
expect(logger).to receive(:error).with /Fixing zip file permissions error.*\n.*output.*\n.*error.*/
138-
139-
expect {
140-
app_packager.unzip(@tmpdir)
141-
}.to raise_error(CloudController::Errors::ApiError, /Invalid zip archive/)
142-
end
143-
end
14496
end
14597

14698
describe '#append_dir_contents' do
@@ -215,7 +167,7 @@
215167
before { FileUtils.cp(File.join(Paths::FIXTURES, 'app_packager_zips', 'bad_directory_permissions.zip'), input_zip) }
216168

217169
it 'deletes all directories from the archive' do
218-
app_packager.fix_subdir_permissions
170+
app_packager.fix_subdir_permissions(@tmpdir, "#{@tmpdir}/application_contents")
219171

220172
has_dirs = Zip::File.open(input_zip) do |in_zip|
221173
in_zip.any?(&:directory?)
@@ -231,7 +183,7 @@
231183
before { FileUtils.cp(File.join(Paths::FIXTURES, 'app_packager_zips', 'special_character_names.zip'), input_zip) }
232184

233185
it 'successfully removes and re-adds them' do
234-
app_packager.fix_subdir_permissions
186+
app_packager.fix_subdir_permissions(@tmpdir, "#{@tmpdir}/application_contents")
235187
expect(`zipinfo #{input_zip}`).to match %r(special_character_names/&&hello::\?\?/)
236188
end
237189
end
@@ -241,12 +193,12 @@
241193

242194
before { FileUtils.cp(File.join(Paths::FIXTURES, 'app_packager_zips', 'many_dirs.zip'), input_zip) }
243195

244-
it 'batches the directory deletes so it does not exceed the max command length' do
196+
it 'fixes the directory permissions and batches the directory deletes so it does not exceed the max command length' do
245197
allow(Open3).to receive(:capture3).and_call_original
246198
batch_size = 10
247199
stub_const('AppPackager::DIRECTORY_DELETE_BATCH_SIZE', batch_size)
248200

249-
app_packager.fix_subdir_permissions
201+
app_packager.fix_subdir_permissions(@tmpdir, "#{@tmpdir}/application_contents")
250202

251203
output = `zipinfo #{input_zip}`
252204

@@ -257,7 +209,7 @@
257209

258210
number_of_batches = (21.0 / batch_size).ceil
259211
expect(number_of_batches).to eq(3)
260-
expect(Open3).to have_received(:capture3).exactly(number_of_batches).times
212+
expect(Open3).to have_received(:capture3).exactly(number_of_batches + 1).times
261213
end
262214
end
263215

@@ -268,7 +220,7 @@
268220
it 'raises an exception' do
269221
allow(Open3).to receive(:capture3).and_return(['output', 'error', double(success?: false)])
270222
expect {
271-
app_packager.fix_subdir_permissions
223+
app_packager.fix_subdir_permissions(@tmpdir, "#{@tmpdir}/application_contents")
272224
}.to raise_error(CloudController::Errors::ApiError, /The app package is invalid: Error removing zip directories./)
273225
end
274226
end
@@ -279,7 +231,7 @@
279231
it 'raises an exception' do
280232
allow(Open3).to receive(:capture3).and_return(['output', 'error', double(success?: false)])
281233
expect {
282-
app_packager.fix_subdir_permissions
234+
app_packager.fix_subdir_permissions(@tmpdir, "#{@tmpdir}/application_contents")
283235
}.to raise_error(CloudController::Errors::ApiError, /The app upload is invalid: Invalid zip archive./)
284236
end
285237
end

spec/unit/lib/cloud_controller/blobstore/fog/fog_client_spec.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,33 @@ def upload_tmpfile(client, key='abcdef')
185185
client.cp_r_to_blobstore(path)
186186
end
187187
end
188+
189+
context 'limit the file mode to those with sufficient permissions' do
190+
subject(:client) do
191+
FogClient.new(connection_config: connection_config,
192+
directory_key: directory_key)
193+
end
194+
195+
it 'copies files with mode >= 0600' do
196+
path = File.join(local_dir, 'file_with_sufficient_permissions')
197+
FileUtils.touch(path)
198+
File.chmod(0600, path)
199+
200+
expect(client).to receive(:exists?)
201+
expect(client).to receive(:cp_to_blobstore)
202+
client.cp_r_to_blobstore(path)
203+
end
204+
205+
it 'does not copy files below the minimum file mode' do
206+
path = File.join(local_dir, 'file_with_insufficient_permissions')
207+
FileUtils.touch(path)
208+
File.chmod(0444, path)
209+
210+
expect(client).not_to receive(:exists?)
211+
expect(client).not_to receive(:cp_to_blobstore)
212+
client.cp_r_to_blobstore(path)
213+
end
214+
end
188215
end
189216

190217
describe '#download_from_blobstore' do

spec/unit/lib/cloud_controller/blobstore/webdav/dav_client_spec.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,48 @@ module Blobstore
389389
end
390390
end
391391

392+
describe 'file permissions' do
393+
context 'sufficient file permissions' do
394+
let!(:sufficiently_permissioned_file) do
395+
Tempfile.open('', source_dir) do |tmpfile|
396+
File.chmod(0600, tmpfile)
397+
tmpfile
398+
end
399+
end
400+
let(:sufficient_perm_file_sha) { Digester.new.digest_path(sufficiently_permissioned_file.path) }
401+
402+
it 'copies files above the maximum size limit' do
403+
allow(response).to receive_messages(status: 201, content: '')
404+
allow(httpclient).to receive(:put).and_return(response)
405+
406+
client.cp_r_to_blobstore(source_dir)
407+
408+
expect(httpclient).to have_received(:put).with(
409+
"http://localhost/admin/droplets/#{sufficient_perm_file_sha[0..1]}/#{sufficient_perm_file_sha[2..3]}/#{sufficient_perm_file_sha}", a_kind_of(File), {})
410+
end
411+
end
412+
413+
context 'insufficient file permissions' do
414+
let!(:insufficiently_permissioned_file) do
415+
Tempfile.open('', source_dir) do |tmpfile|
416+
File.chmod(0444, tmpfile)
417+
tmpfile
418+
end
419+
end
420+
let(:insufficient_perm_file_sha) { Digester.new.digest_path(insufficiently_permissioned_file.path) }
421+
422+
it 'does not copy files above the maximum size limit' do
423+
allow(response).to receive_messages(status: 201, content: '')
424+
allow(httpclient).to receive(:put).and_return(response)
425+
426+
client.cp_r_to_blobstore(source_dir)
427+
428+
expect(httpclient).not_to have_received(:put).with(
429+
"http://localhost/admin/droplets/#{insufficient_perm_file_sha[0..1]}/#{insufficient_perm_file_sha[2..3]}/#{insufficient_perm_file_sha}", a_kind_of(File), {})
430+
end
431+
end
432+
end
433+
392434
context 'when an OpenSSL::SSL::SSLError is raised' do
393435
it 'reraises a BlobstoreError' do
394436
allow(httpclient).to receive(:put).and_raise(OpenSSL::SSL::SSLError.new)

spec/unit/lib/cloud_controller/packager/local_bits_packer_spec.rb

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -294,34 +294,6 @@ module CloudController::Packager
294294
`unzip #{local_tmp_dir}/package.zip path/to/content.txt -d #{local_tmp_dir}`
295295
expect(sprintf('%<mode>o', mode: File.stat(File.join(local_tmp_dir, 'path/to/content.txt')).mode)).to eq('100653')
296296
end
297-
298-
describe 'bad file permissions' do
299-
context 'when the write permissions are too-restrictive' do
300-
let(:mode) { '344' }
301-
302-
it 'errors' do
303-
expect {
304-
packer.send_package_to_blobstore(blobstore_key, uploaded_files_path, cached_files_fingerprints)
305-
}.to raise_error do |error|
306-
expect(error.name).to eq 'AppResourcesFileModeInvalid'
307-
expect(error.response_code).to eq 400
308-
end
309-
end
310-
end
311-
312-
context 'when the permissions are nonsense' do
313-
let(:mode) { 'banana' }
314-
315-
it 'errors' do
316-
expect {
317-
packer.send_package_to_blobstore(blobstore_key, uploaded_files_path, cached_files_fingerprints)
318-
}.to raise_error do |error|
319-
expect(error.name).to eq 'AppResourcesFileModeInvalid'
320-
expect(error.response_code).to eq 400
321-
end
322-
end
323-
end
324-
end
325297
end
326298
end
327299
end

0 commit comments

Comments
 (0)