Commit 41c092d9 by Jon Yurek

Add media-type spoof detection

parent 52840dcd
...@@ -43,6 +43,7 @@ require 'paperclip/attachment' ...@@ -43,6 +43,7 @@ require 'paperclip/attachment'
require 'paperclip/storage' require 'paperclip/storage'
require 'paperclip/callbacks' require 'paperclip/callbacks'
require 'paperclip/file_command_content_type_detector' require 'paperclip/file_command_content_type_detector'
require 'paperclip/media_type_spoof_detector'
require 'paperclip/content_type_detector' require 'paperclip/content_type_detector'
require 'paperclip/glue' require 'paperclip/glue'
require 'paperclip/errors' require 'paperclip/errors'
......
...@@ -166,6 +166,18 @@ module Paperclip ...@@ -166,6 +166,18 @@ module Paperclip
path.respond_to?(:unescape) ? path.unescape : path path.respond_to?(:unescape) ? path.unescape : path
end end
# :nodoc:
def staged_path(style_name = default_style)
if staged?
@queued_for_write[style_name].path
end
end
# :nodoc:
def staged?
! @queued_for_write.empty?
end
# Alias to +url+ # Alias to +url+
def to_s style_name = default_style def to_s style_name = default_style
url(style_name) url(style_name)
...@@ -485,7 +497,7 @@ module Paperclip ...@@ -485,7 +497,7 @@ module Paperclip
end end
end end
# called by storage after the writes are flushed and before @queued_for_writes is cleared # called by storage after the writes are flushed and before @queued_for_write is cleared
def after_flush_writes def after_flush_writes
@queued_for_write.each do |style, file| @queued_for_write.each do |style, file|
file.close unless file.closed? file.close unless file.closed?
......
...@@ -32,10 +32,6 @@ module Paperclip ...@@ -32,10 +32,6 @@ module Paperclip
EMPTY_TYPE EMPTY_TYPE
elsif calculated_type_matches.any? elsif calculated_type_matches.any?
calculated_type_matches.first calculated_type_matches.first
elsif official_type_matches.any?
official_type_matches.first
elsif unofficial_type_matches.any?
unofficial_type_matches.first
else else
type_from_file_command || SENSIBLE_DEFAULT type_from_file_command || SENSIBLE_DEFAULT
end.to_s end.to_s
...@@ -46,7 +42,7 @@ module Paperclip ...@@ -46,7 +42,7 @@ module Paperclip
def empty_file? def empty_file?
File.exists?(@filename) && File.size(@filename) == 0 File.exists?(@filename) && File.size(@filename) == 0
end end
alias :empty? :empty_file? alias :empty? :empty_file?
def blank_name? def blank_name?
...@@ -61,14 +57,6 @@ module Paperclip ...@@ -61,14 +57,6 @@ module Paperclip
possible_types.select{|content_type| content_type == type_from_file_command } possible_types.select{|content_type| content_type == type_from_file_command }
end end
def official_type_matches
possible_types.reject{|content_type| content_type.match(/\/x-/) }
end
def unofficial_type_matches
possible_types.select{|content_type| content_type.match(/\/x-/) }
end
def type_from_file_command def type_from_file_command
@type_from_file_command ||= FileCommandContentTypeDetector.new(@filename).detect @type_from_file_command ||= FileCommandContentTypeDetector.new(@filename).detect
end end
......
...@@ -18,6 +18,7 @@ module Paperclip ...@@ -18,6 +18,7 @@ module Paperclip
register_new_attachment register_new_attachment
add_active_record_callbacks add_active_record_callbacks
add_paperclip_callbacks add_paperclip_callbacks
add_required_validations
end end
private private
...@@ -77,6 +78,10 @@ module Paperclip ...@@ -77,6 +78,10 @@ module Paperclip
Paperclip::AttachmentRegistry.register(@klass, @name, @options) Paperclip::AttachmentRegistry.register(@klass, @name, @options)
end end
def add_required_validations
@klass.validates_media_type_spoof_detection @name
end
def add_active_record_callbacks def add_active_record_callbacks
name = @name name = @name
@klass.send(:after_save) { send(name).send(:save) } @klass.send(:after_save) { send(name).send(:save) }
......
...@@ -34,7 +34,7 @@ module Paperclip ...@@ -34,7 +34,7 @@ module Paperclip
private private
def destination def destination
@destination ||= TempfileFactory.new.generate(original_filename) @destination ||= TempfileFactory.new.generate
end end
def copy_to_tempfile(src) def copy_to_tempfile(src)
......
...@@ -20,11 +20,11 @@ module Paperclip ...@@ -20,11 +20,11 @@ module Paperclip
@size = @tempfile.size || @target.size @size = @tempfile.size || @target.size
end end
def copy_to_tempfile(src) def copy_to_tempfile(source)
if src.respond_to? :copy_to_local_file if source.staged?
src.copy_to_local_file(@style, destination.path) FileUtils.cp(source.staged_path(@style), destination.path)
else else
FileUtils.cp(src.path(@style), destination.path) source.copy_to_local_file(@style, destination.path)
end end
destination destination
end end
......
...@@ -4,19 +4,14 @@ module Paperclip ...@@ -4,19 +4,14 @@ module Paperclip
REGEXP = /\Adata:([-\w]+\/[-\w\+]+);base64,(.*)/m REGEXP = /\Adata:([-\w]+\/[-\w\+]+);base64,(.*)/m
def initialize(target_uri) def initialize(target_uri)
@target_uri = target_uri super(extract_target(target_uri))
cache_current_values
@tempfile = copy_to_tempfile
end end
private private
def cache_current_values def extract_target(uri)
self.original_filename = 'base64.txt' data_uri_parts = uri.match(REGEXP) || []
data_uri_parts ||= @target_uri.match(REGEXP) || [] StringIO.new(Base64.decode64(data_uri_parts[2] || ''))
@content_type = data_uri_parts[1] || 'text/plain'
@target = StringIO.new(Base64.decode64(data_uri_parts[2] || ''))
@size = @target.size
end end
end end
......
...@@ -2,8 +2,8 @@ module Paperclip ...@@ -2,8 +2,8 @@ module Paperclip
class StringioAdapter < AbstractAdapter class StringioAdapter < AbstractAdapter
def initialize(target) def initialize(target)
@target = target @target = target
cache_current_values
@tempfile = copy_to_tempfile @tempfile = copy_to_tempfile
cache_current_values
end end
attr_writer :content_type attr_writer :content_type
...@@ -11,13 +11,10 @@ module Paperclip ...@@ -11,13 +11,10 @@ module Paperclip
private private
def cache_current_values def cache_current_values
@original_filename = @target.original_filename if @target.respond_to?(:original_filename) @content_type = ContentTypeDetector.new(@tempfile.path).detect
@original_filename ||= "stringio.txt" original_filename = @target.original_filename if @target.respond_to?(:original_filename)
self.original_filename = @original_filename.strip original_filename ||= "data.#{extension_for(@content_type)}"
self.original_filename = original_filename.strip
@content_type = @target.content_type if @target.respond_to?(:content_type)
@content_type ||= "text/plain"
@size = @target.size @size = @target.size
end end
...@@ -29,6 +26,11 @@ module Paperclip ...@@ -29,6 +26,11 @@ 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
......
module Paperclip
class MediaTypeSpoofDetector
def self.using(file, name)
new(file, name)
end
def initialize(file, name)
@file = file
@name = name
end
def spoofed?
if ! @name.blank?
! supplied_file_media_type.include?(calculated_media_type)
end
end
private
def supplied_file_media_type
MIME::Types.type_for(@name).collect(&:media_type)
end
def calculated_media_type
type_from_file_command.split("/").first
end
def type_from_file_command
begin
Paperclip.run("file", "-b --mime-type :file", :file => @file.path)
rescue Cocaine::CommandLineError
""
end
end
end
end
module Paperclip module Paperclip
class TempfileFactory class TempfileFactory
def generate(name) def generate(name = random_name)
@name = name @name = name
file = Tempfile.new([basename, extension]) file = Tempfile.new([basename, extension])
file.binmode file.binmode
...@@ -15,5 +15,9 @@ module Paperclip ...@@ -15,5 +15,9 @@ module Paperclip
def basename def basename
Digest::MD5.hexdigest(File.basename(@name, extension)) Digest::MD5.hexdigest(File.basename(@name, extension))
end end
def random_name
SecureRandom.uuid
end
end end
end end
...@@ -3,6 +3,7 @@ require 'active_support/concern' ...@@ -3,6 +3,7 @@ require 'active_support/concern'
require 'paperclip/validators/attachment_content_type_validator' require 'paperclip/validators/attachment_content_type_validator'
require 'paperclip/validators/attachment_presence_validator' require 'paperclip/validators/attachment_presence_validator'
require 'paperclip/validators/attachment_size_validator' require 'paperclip/validators/attachment_size_validator'
require 'paperclip/validators/media_type_spoof_detection_validator'
module Paperclip module Paperclip
module Validators module Validators
......
require 'active_model/validations/presence'
module Paperclip
module Validators
class MediaTypeSpoofDetectionValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
adapter = Paperclip.io_adapters.for(value)
if Paperclip::MediaTypeSpoofDetector.using(adapter, value.original_filename).spoofed?
record.errors.add(attribute, :spoofed_media_type)
end
end
end
module HelperMethods
# Places ActiveModel validations on the presence of a file.
# Options:
# * +if+: A lambda or name of an instance method. Validation will only
# be run if this lambda or method returns true.
# * +unless+: Same as +if+ but validates if lambda or method returns false.
def validates_media_type_spoof_detection(*attr_names)
options = _merge_attributes(attr_names)
validates_with MediaTypeSpoofDetectionValidator, options.dup
validate_before_processing MediaTypeSpoofDetectionValidator, options.dup
end
end
end
end
...@@ -18,16 +18,6 @@ class ContentTypeDetectorTest < Test::Unit::TestCase ...@@ -18,16 +18,6 @@ class ContentTypeDetectorTest < Test::Unit::TestCase
assert_equal "video/mp4", Paperclip::ContentTypeDetector.new(@filename).detect assert_equal "video/mp4", Paperclip::ContentTypeDetector.new(@filename).detect
end end
should 'find the first result that matches from the official types' do
@filename = "/path/to/something.bmp"
assert_equal "image/bmp", Paperclip::ContentTypeDetector.new(@filename).detect
end
should 'find the first unofficial result for this filename if no official ones exist' do
@filename = "/path/to/something.aiff"
assert_equal "audio/x-aiff", Paperclip::ContentTypeDetector.new(@filename).detect
end
should 'find the right type in the list via the file command' do should 'find the right type in the list via the file command' do
@filename = "#{Dir.tmpdir}/something.hahalolnotreal" @filename = "#{Dir.tmpdir}/something.hahalolnotreal"
File.open(@filename, "w+") do |file| File.open(@filename, "w+") do |file|
......
...@@ -13,6 +13,7 @@ class AbstractAdapterTest < Test::Unit::TestCase ...@@ -13,6 +13,7 @@ class AbstractAdapterTest < Test::Unit::TestCase
setup do setup do
@adapter = TestAdapter.new @adapter = TestAdapter.new
@adapter.stubs(:path).returns("image.png") @adapter.stubs(:path).returns("image.png")
Paperclip.stubs(:run).returns("image/png\n")
end end
should "return the content type without newline" do should "return the content type without newline" do
......
...@@ -75,7 +75,7 @@ class AttachmentAdapterTest < Test::Unit::TestCase ...@@ -75,7 +75,7 @@ class AttachmentAdapterTest < Test::Unit::TestCase
end end
should "not generate paths that include restricted characters" do should "not generate paths that include restricted characters" do
assert_no_match /:/, @subject.path assert_no_match(/:/, @subject.path)
end end
should "not generate filenames that include restricted characters" do should "not generate filenames that include restricted characters" do
......
require './test/helper'
class MediaTypeSpoofDetectorTest < Test::Unit::TestCase
should 'reject a file that is named .html and identifies as PNG' do
file = File.open(fixture_file("5k.png"))
assert Paperclip::MediaTypeSpoofDetector.using(file, "5k.html").spoofed?
end
should 'not reject a file that is named .jpg and identifies as PNG' do
file = File.open(fixture_file("5k.png"))
assert ! Paperclip::MediaTypeSpoofDetector.using(file, "5k.jpg").spoofed?
end
should 'not reject a file that is named .html and identifies as HTML' do
file = File.open(fixture_file("empty.html"))
assert ! Paperclip::MediaTypeSpoofDetector.using(file, "empty.html").spoofed?
end
should 'not reject a file that does not have a name' do
file = File.open(fixture_file("empty.html"))
assert ! Paperclip::MediaTypeSpoofDetector.using(file, "").spoofed?
end
should 'not reject when the supplied file is an IOAdapter' do
adapter = Paperclip.io_adapters.for(File.new(fixture_file("5k.png")))
assert ! Paperclip::MediaTypeSpoofDetector.using(adapter, adapter.original_filename).spoofed?
end
end
...@@ -3,15 +3,27 @@ require './test/helper' ...@@ -3,15 +3,27 @@ require './test/helper'
class Paperclip::TempfileFactoryTest < Test::Unit::TestCase class Paperclip::TempfileFactoryTest < Test::Unit::TestCase
should "be able to generate a tempfile with the right name" do should "be able to generate a tempfile with the right name" do
file = subject.generate("omg.png") file = subject.generate("omg.png")
assert File.extname(file.path), "png"
end end
should "be able to generate a tempfile with the right name with a tilde at the beginning" do should "be able to generate a tempfile with the right name with a tilde at the beginning" do
file = subject.generate("~omg.png") file = subject.generate("~omg.png")
assert File.extname(file.path), "png"
end end
should "be able to generate a tempfile with the right name with a tilde at the end" do should "be able to generate a tempfile with the right name with a tilde at the end" do
file = subject.generate("omg.png~") file = subject.generate("omg.png~")
assert File.extname(file.path), "png"
end end
should "be able to generate a tempfile from a file with a really long name" do should "be able to generate a tempfile from a file with a really long name" do
filename = "#{"longfilename" * 100}.txt" filename = "#{"longfilename" * 100}.png"
file = subject.generate(filename) file = subject.generate(filename)
assert File.extname(file.path), "png"
end
should 'be able to take nothing as a parameter and not error' do
file = subject.generate
assert File.exists?(file.path)
end end
end end
...@@ -60,7 +60,7 @@ class AttachmentPresenceValidatorTest < Test::Unit::TestCase ...@@ -60,7 +60,7 @@ class AttachmentPresenceValidatorTest < Test::Unit::TestCase
context "with attachment" do context "with attachment" do
setup do setup do
build_validator build_validator
@dummy.avatar = StringIO.new('.') @dummy.avatar = StringIO.new('.\n')
@validator.validate(@dummy) @validator.validate(@dummy)
end end
......
require './test/helper'
class MediaTypeSpoofDetectionValidatorTest < Test::Unit::TestCase
def setup
rebuild_model
@dummy = Dummy.new
end
should "be on the attachment without being explicitly added" do
assert Dummy.validators_on(:avatar).any?{ |validator| validator.kind == :media_type_spoof_detection }
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