Skip to content
This repository has been archived by the owner on Jul 13, 2023. It is now read-only.

Commit

Permalink
Add media-type spoof detection
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Yurek committed Jan 29, 2014
1 parent 52840dc commit 41c092d
Show file tree
Hide file tree
Showing 20 changed files with 165 additions and 50 deletions.
1 change: 1 addition & 0 deletions lib/paperclip.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
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'
Expand Down
14 changes: 13 additions & 1 deletion lib/paperclip/attachment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,18 @@ def path(style_name = default_style)
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)
Expand Down Expand Up @@ -485,7 +497,7 @@ def flush_errors #:nodoc:
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?
Expand Down
14 changes: 1 addition & 13 deletions lib/paperclip/content_type_detector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ def detect
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
Expand All @@ -46,7 +42,7 @@ def detect
def empty_file?
File.exists?(@filename) && File.size(@filename) == 0
end

alias :empty? :empty_file?

def blank_name?
Expand All @@ -61,14 +57,6 @@ def calculated_type_matches
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
Expand Down
5 changes: 5 additions & 0 deletions lib/paperclip/has_attached_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def define
register_new_attachment
add_active_record_callbacks
add_paperclip_callbacks
add_required_validations
end

private
Expand Down Expand Up @@ -77,6 +78,10 @@ def register_new_attachment
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) }
Expand Down
2 changes: 1 addition & 1 deletion lib/paperclip/io_adapters/abstract_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def assignment?
private

def destination
@destination ||= TempfileFactory.new.generate(original_filename)
@destination ||= TempfileFactory.new.generate
end

def copy_to_tempfile(src)
Expand Down
8 changes: 4 additions & 4 deletions lib/paperclip/io_adapters/attachment_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ def cache_current_values
@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
Expand Down
13 changes: 4 additions & 9 deletions lib/paperclip/io_adapters/data_uri_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,14 @@ class DataUriAdapter < StringioAdapter
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
Expand Down
18 changes: 10 additions & 8 deletions lib/paperclip/io_adapters/stringio_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,19 @@ 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

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

Expand All @@ -29,6 +26,11 @@ def copy_to_tempfile
destination
end

def extension_for(content_type)
type = MIME::Types[content_type].first
type && type.extensions.first
end

end
end

Expand Down
36 changes: 36 additions & 0 deletions lib/paperclip/media_type_spoof_detector.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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
6 changes: 5 additions & 1 deletion lib/paperclip/tempfile_factory.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Paperclip
class TempfileFactory

def generate(name)
def generate(name = random_name)
@name = name
file = Tempfile.new([basename, extension])
file.binmode
Expand All @@ -15,5 +15,9 @@ def extension
def basename
Digest::MD5.hexdigest(File.basename(@name, extension))
end

def random_name
SecureRandom.uuid
end
end
end
1 change: 1 addition & 0 deletions lib/paperclip/validators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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
Expand Down
27 changes: 27 additions & 0 deletions lib/paperclip/validators/media_type_spoof_detection_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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
10 changes: 0 additions & 10 deletions test/content_type_detector_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/empty.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<html></html>
1 change: 1 addition & 0 deletions test/io_adapters/abstract_adapter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def content_type
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
Expand Down
2 changes: 1 addition & 1 deletion test/io_adapters/attachment_adapter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def setup
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
Expand Down
28 changes: 28 additions & 0 deletions test/media_type_spoof_detector_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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
14 changes: 13 additions & 1 deletion test/tempfile_factory_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,27 @@
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
2 changes: 1 addition & 1 deletion test/validators/attachment_presence_validator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def build_validator(options={})
context "with attachment" do
setup do
build_validator
@dummy.avatar = StringIO.new('.')
@dummy.avatar = StringIO.new('.\n')
@validator.validate(@dummy)
end

Expand Down
12 changes: 12 additions & 0 deletions test/validators/media_type_spoof_detection_validator_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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

0 comments on commit 41c092d

Please sign in to comment.