Commit 41c092d9 by Jon Yurek

Add media-type spoof detection

parent 52840dcd
......@@ -43,6 +43,7 @@ require 'paperclip/attachment'
require 'paperclip/storage'
require 'paperclip/callbacks'
require 'paperclip/file_command_content_type_detector'
require 'paperclip/media_type_spoof_detector'
require 'paperclip/content_type_detector'
require 'paperclip/glue'
require 'paperclip/errors'
......
......@@ -166,6 +166,18 @@ module Paperclip
path.respond_to?(:unescape) ? path.unescape : path
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+
def to_s style_name = default_style
url(style_name)
......@@ -485,7 +497,7 @@ module Paperclip
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
@queued_for_write.each do |style, file|
file.close unless file.closed?
......
......@@ -32,10 +32,6 @@ module Paperclip
EMPTY_TYPE
elsif calculated_type_matches.any?
calculated_type_matches.first
elsif official_type_matches.any?
official_type_matches.first
elsif unofficial_type_matches.any?
unofficial_type_matches.first
else
type_from_file_command || SENSIBLE_DEFAULT
end.to_s
......@@ -46,7 +42,7 @@ module Paperclip
def empty_file?
File.exists?(@filename) && File.size(@filename) == 0
end
alias :empty? :empty_file?
def blank_name?
......@@ -61,14 +57,6 @@ module Paperclip
possible_types.select{|content_type| content_type == type_from_file_command }
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
@type_from_file_command ||= FileCommandContentTypeDetector.new(@filename).detect
end
......
......@@ -18,6 +18,7 @@ module Paperclip
register_new_attachment
add_active_record_callbacks
add_paperclip_callbacks
add_required_validations
end
private
......@@ -77,6 +78,10 @@ module Paperclip
Paperclip::AttachmentRegistry.register(@klass, @name, @options)
end
def add_required_validations
@klass.validates_media_type_spoof_detection @name
end
def add_active_record_callbacks
name = @name
@klass.send(:after_save) { send(name).send(:save) }
......
......@@ -34,7 +34,7 @@ module Paperclip
private
def destination
@destination ||= TempfileFactory.new.generate(original_filename)
@destination ||= TempfileFactory.new.generate
end
def copy_to_tempfile(src)
......
......@@ -20,11 +20,11 @@ module Paperclip
@size = @tempfile.size || @target.size
end
def copy_to_tempfile(src)
if src.respond_to? :copy_to_local_file
src.copy_to_local_file(@style, destination.path)
def copy_to_tempfile(source)
if source.staged?
FileUtils.cp(source.staged_path(@style), destination.path)
else
FileUtils.cp(src.path(@style), destination.path)
source.copy_to_local_file(@style, destination.path)
end
destination
end
......
......@@ -4,19 +4,14 @@ module Paperclip
REGEXP = /\Adata:([-\w]+\/[-\w\+]+);base64,(.*)/m
def initialize(target_uri)
@target_uri = target_uri
cache_current_values
@tempfile = copy_to_tempfile
super(extract_target(target_uri))
end
private
def cache_current_values
self.original_filename = 'base64.txt'
data_uri_parts ||= @target_uri.match(REGEXP) || []
@content_type = data_uri_parts[1] || 'text/plain'
@target = StringIO.new(Base64.decode64(data_uri_parts[2] || ''))
@size = @target.size
def extract_target(uri)
data_uri_parts = uri.match(REGEXP) || []
StringIO.new(Base64.decode64(data_uri_parts[2] || ''))
end
end
......
......@@ -2,8 +2,8 @@ module Paperclip
class StringioAdapter < AbstractAdapter
def initialize(target)
@target = target
cache_current_values
@tempfile = copy_to_tempfile
cache_current_values
end
attr_writer :content_type
......@@ -11,13 +11,10 @@ module Paperclip
private
def cache_current_values
@original_filename = @target.original_filename if @target.respond_to?(:original_filename)
@original_filename ||= "stringio.txt"
self.original_filename = @original_filename.strip
@content_type = @target.content_type if @target.respond_to?(:content_type)
@content_type ||= "text/plain"
@content_type = ContentTypeDetector.new(@tempfile.path).detect
original_filename = @target.original_filename if @target.respond_to?(:original_filename)
original_filename ||= "data.#{extension_for(@content_type)}"
self.original_filename = original_filename.strip
@size = @target.size
end
......@@ -29,6 +26,11 @@ module Paperclip
destination
end
def extension_for(content_type)
type = MIME::Types[content_type].first
type && type.extensions.first
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
class TempfileFactory
def generate(name)
def generate(name = random_name)
@name = name
file = Tempfile.new([basename, extension])
file.binmode
......@@ -15,5 +15,9 @@ module Paperclip
def basename
Digest::MD5.hexdigest(File.basename(@name, extension))
end
def random_name
SecureRandom.uuid
end
end
end
......@@ -3,6 +3,7 @@ require 'active_support/concern'
require 'paperclip/validators/attachment_content_type_validator'
require 'paperclip/validators/attachment_presence_validator'
require 'paperclip/validators/attachment_size_validator'
require 'paperclip/validators/media_type_spoof_detection_validator'
module Paperclip
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
assert_equal "video/mp4", Paperclip::ContentTypeDetector.new(@filename).detect
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
@filename = "#{Dir.tmpdir}/something.hahalolnotreal"
File.open(@filename, "w+") do |file|
......
......@@ -13,6 +13,7 @@ class AbstractAdapterTest < Test::Unit::TestCase
setup do
@adapter = TestAdapter.new
@adapter.stubs(:path).returns("image.png")
Paperclip.stubs(:run).returns("image/png\n")
end
should "return the content type without newline" do
......
......@@ -75,7 +75,7 @@ class AttachmentAdapterTest < Test::Unit::TestCase
end
should "not generate paths that include restricted characters" do
assert_no_match /:/, @subject.path
assert_no_match(/:/, @subject.path)
end
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'
class Paperclip::TempfileFactoryTest < Test::Unit::TestCase
should "be able to generate a tempfile with the right name" do
file = subject.generate("omg.png")
assert File.extname(file.path), "png"
end
should "be able to generate a tempfile with the right name with a tilde at the beginning" do
file = subject.generate("~omg.png")
assert File.extname(file.path), "png"
end
should "be able to generate a tempfile with the right name with a tilde at the end" do
file = subject.generate("omg.png~")
assert File.extname(file.path), "png"
end
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)
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
......@@ -60,7 +60,7 @@ class AttachmentPresenceValidatorTest < Test::Unit::TestCase
context "with attachment" do
setup do
build_validator
@dummy.avatar = StringIO.new('.')
@dummy.avatar = StringIO.new('.\n')
@validator.validate(@dummy)
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