Skip to content

Commit beeaf43

Browse files
committed
extra security check - check db for saved file names
1 parent 51bf9f6 commit beeaf43

File tree

3 files changed

+41
-3
lines changed

3 files changed

+41
-3
lines changed

app/controllers/uploads_controller.rb

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class UploadsController < ApplicationController
2121
def download
2222
sanitize_filepath
2323
parse_upload_path
24+
ensure_valid_db_record
2425
track_download if trackable_request?
2526
send_file @sanitized_filepath, disposition: :inline
2627
end
@@ -46,10 +47,38 @@ def parse_upload_path
4647
raise_not_found_exception if path_parts.length < 4
4748

4849
model_key = path_parts[0].downcase
49-
@model_name = MODELS_OVERRIDES[model_key] || model_key
50+
@model_name = (MODELS_OVERRIDES[model_key] || model_key).downcase
51+
@uploader_name = path_parts[1].downcase
52+
@record_id = path_parts[2].to_i
5053
@filename = path_parts[3..].join("/")
5154
end
5255

56+
# extra security step, do not let download antything for removed records or not saved in DB
57+
# we already have sanitized file path that should be enough, but this also will ensure no protected file is downloaded
58+
def ensure_valid_db_record
59+
raise_not_found_exception unless allowed_models.include?(@model_name)
60+
61+
model_class = @model_name.classify.constantize
62+
raise_not_found_exception unless model_class.uploaders.keys.map(&:to_s).include?(@uploader_name) # ensure valid uploader
63+
64+
record = model_class.find(@record_id)
65+
uploader = record.public_send(@uploader_name)
66+
db_filenames = [uploader.file.file, *uploader.versions.values.map { |v| v.file.file }].map { |f| File.basename(f) }
67+
68+
unless db_filenames.include?(File.basename(@sanitized_filepath))
69+
raise_not_found_exception
70+
end
71+
rescue NameError, ActiveRecord::RecordNotFound
72+
raise_not_found_exception
73+
end
74+
75+
def allowed_models
76+
Dir.entries(Rails.root.join("uploads"))
77+
.select { |entry| File.directory?(Rails.root.join("uploads", entry)) }
78+
.reject { |entry| entry.start_with?(".") || entry == "tmp" }
79+
.map { |entry| MODELS_OVERRIDES[entry.downcase] || entry }
80+
end
81+
5382
def track_download
5483
TrackFileDownloadJob.perform_later(client_id, request.url, @filename, @model_name)
5584
end

spec/factories/contributors.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
priority { rand(0..10) }
2222
logo { Rack::Test::UploadedFile.new(File.join(Rails.root, "spec", "support", "files", "image.png")) }
2323

24-
factory :partner do
24+
factory :partner, class: Partner do
2525
type { "Partner" }
2626
end
2727

28-
factory :donor do
28+
factory :donor, class: Donor do
2929
type { "Donor" }
3030
end
3131
end

spec/integration/uploads_spec.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@
4040

4141
expect(response).to have_http_status(:not_found)
4242
end
43+
44+
it "returns 404 for existing files but not in db" do
45+
new_file = File.join(File.dirname(@document_file.attachment.file.file), "newfile.txt")
46+
File.write(new_file, "test")
47+
48+
get "/uploads/operator_document_file/attachment/#{@document_file.id}/newfile.txt"
49+
50+
expect(response).to have_http_status(:not_found)
51+
end
4352
end
4453

4554
context "invalid path handling" do

0 commit comments

Comments
 (0)