Commit 263a4981 by Jon Yurek

Don't spoof-check files with no extensions

There's nothing they can be spoofing, and there's little danger of a
webserver accidentally sending a dangerous Content-Type with a file that
has no extension. If the browser chooses to interpret it in a dangerous
manner, no server-side library on earth can change that.

This isn't specifically related to RSpec, but is built off the branch
and is part of an effort to get the CI build green.
parent 743e2e00
...@@ -101,6 +101,14 @@ module Paperclip ...@@ -101,6 +101,14 @@ module Paperclip
File.extname(attachment.original_filename).gsub(/\A\.+/, "") File.extname(attachment.original_filename).gsub(/\A\.+/, "")
end end
# Returns the dot+extension of the file. e.g. ".jpg" for "file.jpg"
# If the style has a format defined, it will return the format instead
# of the actual extension. If the extension is empty, no dot is added.
def dotextension attachment, style_name
ext = extension(attachment, style_name)
ext.empty? ? "" : ".#{ext}"
end
# Returns an extension based on the content type. e.g. "jpeg" for # Returns an extension based on the content type. e.g. "jpeg" for
# "image/jpeg". If the style has a specified format, it will override the # "image/jpeg". If the style has a specified format, it will override the
# content-type detection. # content-type detection.
......
...@@ -13,7 +13,7 @@ module Paperclip ...@@ -13,7 +13,7 @@ module Paperclip
def cache_current_values def cache_current_values
@content_type = ContentTypeDetector.new(@tempfile.path).detect @content_type = ContentTypeDetector.new(@tempfile.path).detect
original_filename = @target.original_filename if @target.respond_to?(:original_filename) original_filename = @target.original_filename if @target.respond_to?(:original_filename)
original_filename ||= "data.#{extension_for(@content_type)}" original_filename ||= "data"
self.original_filename = original_filename.strip self.original_filename = original_filename.strip
@size = @target.size @size = @target.size
end end
...@@ -26,11 +26,6 @@ module Paperclip ...@@ -26,11 +26,6 @@ module Paperclip
destination destination
end end
def extension_for(content_type)
type = MIME::Types[content_type].first
type && type.extensions.first
end
end end
end end
......
...@@ -10,7 +10,7 @@ module Paperclip ...@@ -10,7 +10,7 @@ module Paperclip
end end
def spoofed? def spoofed?
if @name.present? && media_type_mismatch? && mapping_override_mismatch? if has_name? && has_extension? && media_type_mismatch? && mapping_override_mismatch?
Paperclip.log("Content Type Spoof: Filename #{File.basename(@name)} (#{supplied_file_content_types}), content type discovered from file command: #{calculated_content_type}. See documentation to allow this combination.") Paperclip.log("Content Type Spoof: Filename #{File.basename(@name)} (#{supplied_file_content_types}), content type discovered from file command: #{calculated_content_type}. See documentation to allow this combination.")
true true
end end
...@@ -18,6 +18,14 @@ module Paperclip ...@@ -18,6 +18,14 @@ module Paperclip
private private
def has_name?
@name.present?
end
def has_extension?
File.extname(@name).present?
end
def media_type_mismatch? def media_type_mismatch?
! supplied_file_media_types.include?(calculated_media_type) ! supplied_file_media_types.include?(calculated_media_type)
end end
......
...@@ -337,7 +337,11 @@ describe Paperclip::Attachment do ...@@ -337,7 +337,11 @@ describe Paperclip::Attachment do
context "An attachment with :hash interpolations" do context "An attachment with :hash interpolations" do
before do before do
@file = StringIO.new("...\n") @file = File.open(fixture_file("5k.png"))
end
after do
@file.close
end end
it "raises if no secret is provided" do it "raises if no secret is provided" do
...@@ -360,15 +364,15 @@ describe Paperclip::Attachment do ...@@ -360,15 +364,15 @@ describe Paperclip::Attachment do
end end
it "results in the correct interpolation" do it "results in the correct interpolation" do
assert_equal "dummies/avatars/original/data.txt", assert_equal "dummies/avatars/original/5k.png",
@attachment.send(:interpolate, @attachment.options[:hash_data]) @attachment.send(:interpolate, @attachment.options[:hash_data])
assert_equal "dummies/avatars/thumb/data.txt", assert_equal "dummies/avatars/thumb/5k.png",
@attachment.send(:interpolate, @attachment.options[:hash_data], :thumb) @attachment.send(:interpolate, @attachment.options[:hash_data], :thumb)
end end
it "results in a correct hash" do it "results in a correct hash" do
assert_equal "e1079a5c34ddbd197ebd0280d07952d98a57fb30", @attachment.path assert_equal "0a59e9142bba11576de1d353d8747b1acad5ad34", @attachment.path
assert_equal "d740189bd3e49ef226fab84c8d45f7ae4126d043", @attachment.path(:thumb) assert_equal "b39a062c1e62e85a6c785ed00cf3bebf5f850e2b", @attachment.path(:thumb)
end end
end end
end end
......
...@@ -71,6 +71,20 @@ describe Paperclip::Interpolations do ...@@ -71,6 +71,20 @@ describe Paperclip::Interpolations do
assert_equal "jpe", interpolations.content_type_extension(attachment, :style) assert_equal "jpe", interpolations.content_type_extension(attachment, :style)
end end
it "returns the extension of the file with a dot" do
attachment = mock
attachment.expects(:original_filename).returns("one.jpg")
attachment.expects(:styles).returns({})
assert_equal ".jpg", Paperclip::Interpolations.dotextension(attachment, :style)
end
it "returns the extension of the file without a dot if the extension is empty" do
attachment = mock
attachment.expects(:original_filename).returns("one")
attachment.expects(:styles).returns({})
assert_equal "", Paperclip::Interpolations.dotextension(attachment, :style)
end
it "returns the latter half of the content type of the extension if no match found" do it "returns the latter half of the content type of the extension if no match found" do
attachment = mock attachment = mock
attachment.expects(:content_type).at_least_once().returns('not/found') attachment.expects(:content_type).at_least_once().returns('not/found')
......
...@@ -18,8 +18,8 @@ describe Paperclip::DataUriAdapter do ...@@ -18,8 +18,8 @@ describe Paperclip::DataUriAdapter do
@subject = Paperclip.io_adapters.for(@contents) @subject = Paperclip.io_adapters.for(@contents)
end end
it "returns a file name based on the content type" do it "returns a nondescript file name" do
assert_equal "data.png", @subject.original_filename assert_equal "data", @subject.original_filename
end end
it "returns a content type" do it "returns a content type" do
......
...@@ -9,7 +9,7 @@ describe Paperclip::StringioAdapter do ...@@ -9,7 +9,7 @@ describe Paperclip::StringioAdapter do
end end
it "returns a file name" do it "returns a file name" do
assert_equal "data.txt", @subject.original_filename assert_equal "data", @subject.original_filename
end end
it "returns a content type" do it "returns a content type" do
......
...@@ -21,6 +21,11 @@ describe Paperclip::MediaTypeSpoofDetector do ...@@ -21,6 +21,11 @@ describe Paperclip::MediaTypeSpoofDetector do
assert ! Paperclip::MediaTypeSpoofDetector.using(file, "").spoofed? assert ! Paperclip::MediaTypeSpoofDetector.using(file, "").spoofed?
end end
it 'does not reject a file that does have an extension' do
file = File.open(fixture_file("empty.html"))
assert ! Paperclip::MediaTypeSpoofDetector.using(file, "data").spoofed?
end
it 'does not reject when the supplied file is an IOAdapter' do it 'does not reject when the supplied file is an IOAdapter' do
adapter = Paperclip.io_adapters.for(File.new(fixture_file("5k.png"))) adapter = Paperclip.io_adapters.for(File.new(fixture_file("5k.png")))
assert ! Paperclip::MediaTypeSpoofDetector.using(adapter, adapter.original_filename).spoofed? assert ! Paperclip::MediaTypeSpoofDetector.using(adapter, adapter.original_filename).spoofed?
......
...@@ -130,7 +130,7 @@ describe Paperclip::Storage::Fog do ...@@ -130,7 +130,7 @@ describe Paperclip::Storage::Fog do
fog_credentials: @credentials, fog_credentials: @credentials,
fog_host: nil, fog_host: nil,
fog_file: {cache_control: 1234}, fog_file: {cache_control: 1234},
path: ":attachment/:basename.:extension", path: ":attachment/:basename:dotextension",
storage: :fog storage: :fog
} }
...@@ -227,7 +227,7 @@ describe Paperclip::Storage::Fog do ...@@ -227,7 +227,7 @@ describe Paperclip::Storage::Fog do
end end
it "provides a public url" do it "provides a public url" do
assert @dummy.avatar.url =~ /^http:\/\/example\.com\/avatars\/data\.txt\?\d*$/ expect(@dummy.avatar.url).to match(/^http:\/\/example\.com\/avatars\/data\?\d*$/)
end end
end end
...@@ -237,7 +237,7 @@ describe Paperclip::Storage::Fog do ...@@ -237,7 +237,7 @@ describe Paperclip::Storage::Fog do
fog_directory: @fog_directory, fog_directory: @fog_directory,
fog_credentials: @credentials, fog_credentials: @credentials,
fog_host: 'http://img%d.example.com', fog_host: 'http://img%d.example.com',
path: ":attachment/:basename.:extension", path: ":attachment/:basename:dotextension",
storage: :fog storage: :fog
) )
@dummy = Dummy.new @dummy = Dummy.new
...@@ -246,7 +246,7 @@ describe Paperclip::Storage::Fog do ...@@ -246,7 +246,7 @@ describe Paperclip::Storage::Fog do
end end
it "provides a public url" do it "provides a public url" do
assert @dummy.avatar.url =~ /^http:\/\/img[0123]\.example\.com\/avatars\/data\.txt\?\d*$/ expect(@dummy.avatar.url).to match(/^http:\/\/img[0123]\.example\.com\/avatars\/data\?\d*$/)
end end
end end
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment