Skip to content

Commit af72e6f

Browse files
authored
Merge pull request #3376 from AlchemyCMS/sanitize-filenames
Sanitize filenames before upload
2 parents dbc9672 + b815e13 commit af72e6f

6 files changed

Lines changed: 45 additions & 0 deletions

File tree

app/models/alchemy/storage_adapter/dragonfly.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ def self.included(base)
1515

1616
has_many :thumbs, class_name: "Alchemy::PictureThumb", dependent: :destroy
1717

18+
before_save do
19+
self.image_file_name = sanitized_filename(image_file_name)
20+
end
21+
1822
# Create important thumbnails upfront
1923
after_create -> { PictureThumb.generate_thumbs!(self) },
2024
if: :has_convertible_format?
@@ -30,6 +34,10 @@ def self.included(base)
3034
write_attribute(:file_mime_type, file.mime_type)
3135
}
3236
end
37+
38+
before_save do
39+
self.file_name = sanitized_filename(file_name)
40+
end
3341
end
3442
end
3543
end

lib/alchemy/name_conversions.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,11 @@ def convert_to_urlname(name)
2222
def convert_to_humanized_name(name, suffix)
2323
name.gsub(/\.#{::Regexp.quote(suffix)}$/i, "").tr("_", " ").strip
2424
end
25+
26+
# Sanitizes a given filename by removing directory traversal attempts and HTML entities.
27+
def sanitized_filename(file_name)
28+
file_name = File.basename(file_name)
29+
CGI.escapeHTML(file_name)
30+
end
2531
end
2632
end

spec/models/alchemy/attachment_spec.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ module Alchemy
1010
build(:alchemy_attachment, file:, name: nil, file_name: nil)
1111
end
1212

13+
if Alchemy.storage_adapter.dragonfly?
14+
it_behaves_like "having file name sanitization" do
15+
subject { Attachment.new(file:) }
16+
let(:file_name_attribute) { :file_name }
17+
end
18+
end
19+
1320
it_behaves_like "a relatable resource",
1421
resource_name: :attachment,
1522
ingredient_type: :file

spec/models/alchemy/picture_spec.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ module Alchemy
1010

1111
let(:picture) { build(:alchemy_picture, image_file: image_file) }
1212

13+
if Alchemy.storage_adapter.dragonfly?
14+
it_behaves_like "having file name sanitization" do
15+
subject { Picture.new(image_file:) }
16+
let(:file_name_attribute) { :image_file_name }
17+
end
18+
end
19+
1320
it_behaves_like "a relatable resource",
1421
resource_name: :picture,
1522
ingredient_type: :picture

spec/rails_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
require "alchemy/test_support/shared_uploader_examples"
3535
require "alchemy/test_support/current_language_shared_examples"
3636

37+
require_relative "support/file_name_examples"
3738
require_relative "support/hint_examples"
3839
require_relative "support/custom_news_elements_finder"
3940

spec/support/file_name_examples.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
RSpec.shared_examples_for "having file name sanitization" do
6+
describe "file name sanitization" do
7+
let(:invalid_file_name) { 'some/../path"<script>alert(1)</script>.png' }
8+
let(:sanitized_file_name) { "script&gt;.png" }
9+
10+
it "sanitizes the file name before saving" do
11+
subject.send("#{file_name_attribute}=", invalid_file_name)
12+
subject.save
13+
expect(subject.send(file_name_attribute)).to eq(sanitized_file_name)
14+
end
15+
end
16+
end

0 commit comments

Comments
 (0)