paperclip 3.5.4 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of paperclip might be problematic. Click here for more details.

Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/LICENSE +1 -3
  4. data/NEWS +25 -21
  5. data/README.md +35 -1
  6. data/features/step_definitions/attachment_steps.rb +2 -0
  7. data/features/step_definitions/rails_steps.rb +1 -0
  8. data/lib/paperclip.rb +1 -0
  9. data/lib/paperclip/attachment.rb +25 -1
  10. data/lib/paperclip/callbacks.rb +1 -1
  11. data/lib/paperclip/content_type_detector.rb +1 -13
  12. data/lib/paperclip/errors.rb +5 -0
  13. data/lib/paperclip/has_attached_file.rb +5 -0
  14. data/lib/paperclip/io_adapters/abstract_adapter.rb +1 -1
  15. data/lib/paperclip/io_adapters/attachment_adapter.rb +4 -4
  16. data/lib/paperclip/io_adapters/data_uri_adapter.rb +4 -9
  17. data/lib/paperclip/io_adapters/stringio_adapter.rb +10 -8
  18. data/lib/paperclip/media_type_spoof_detector.rb +36 -0
  19. data/lib/paperclip/tempfile_factory.rb +5 -1
  20. data/lib/paperclip/validators.rb +6 -1
  21. data/lib/paperclip/validators/attachment_content_type_validator.rb +4 -0
  22. data/lib/paperclip/validators/attachment_file_name_validator.rb +80 -0
  23. data/lib/paperclip/validators/attachment_file_type_ignorance_validator.rb +29 -0
  24. data/lib/paperclip/validators/attachment_presence_validator.rb +4 -0
  25. data/lib/paperclip/validators/attachment_size_validator.rb +4 -0
  26. data/lib/paperclip/validators/media_type_spoof_detection_validator.rb +27 -0
  27. data/lib/paperclip/version.rb +1 -1
  28. data/test/attachment_definitions_test.rb +1 -0
  29. data/test/attachment_test.rb +40 -43
  30. data/test/content_type_detector_test.rb +0 -10
  31. data/test/fixtures/empty.html +1 -0
  32. data/test/has_attached_file_test.rb +3 -1
  33. data/test/helper.rb +11 -4
  34. data/test/io_adapters/abstract_adapter_test.rb +1 -0
  35. data/test/io_adapters/attachment_adapter_test.rb +1 -1
  36. data/test/io_adapters/data_uri_adapter_test.rb +2 -2
  37. data/test/io_adapters/file_adapter_test.rb +0 -11
  38. data/test/io_adapters/http_url_proxy_adapter_test.rb +2 -3
  39. data/test/io_adapters/stringio_adapter_test.rb +1 -1
  40. data/test/matchers/have_attached_file_matcher_test.rb +3 -2
  41. data/test/matchers/validate_attachment_content_type_matcher_test.rb +13 -12
  42. data/test/matchers/validate_attachment_presence_matcher_test.rb +8 -7
  43. data/test/matchers/validate_attachment_size_matcher_test.rb +12 -11
  44. data/test/media_type_spoof_detector_test.rb +28 -0
  45. data/test/meta_class_test.rb +2 -2
  46. data/test/schema_test.rb +6 -0
  47. data/test/storage/fog_test.rb +4 -4
  48. data/test/storage/s3_test.rb +32 -31
  49. data/test/tempfile_factory_test.rb +13 -1
  50. data/test/validators/attachment_file_name_validator_test.rb +162 -0
  51. data/test/validators/attachment_presence_validator_test.rb +1 -1
  52. data/test/validators/media_type_spoof_detection_validator_test.rb +12 -0
  53. data/test/validators_test.rb +43 -3
  54. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 302d782024b65e52c6e3b0f2caefe6c96893fda6
4
- data.tar.gz: 11e787c6934c7ff8a44409ba54cc049e7743309c
3
+ metadata.gz: 748ce579f297e0d58f7ee6e72b2968341af560be
4
+ data.tar.gz: 47ed54affca1e4c7736eddcb33dfac09763d871d
5
5
  SHA512:
6
- metadata.gz: 0b7b11bca1472987b99c21656db9dd8f1258180078ca9811c7fa977b2b0897c724b069806a058a936873ddc9aac017c133de8da2f8946952a04f9fbe110571d5
7
- data.tar.gz: f52bf9968bbbca1d59237055a66a62acfb882fab61db9060211a2361ce6f01fa3e3b325a622f9e905a4a60f845c7f03595d19e106898e9a6f6b45a2d66964f48
6
+ metadata.gz: 1337da62c00c9b10e151b8a1efd10b611d92c0601c3471bab99b57acbdf7c532e9714ce271c1bc01dbbc499aa71938ba5865b15ff44f1f68d7c0e919e24d00b8
7
+ data.tar.gz: e8fe2a23b8bc5fbbfc9e5ead740a42a6ff7ccb93c9cc452e10cd443b510560ac826d6ef1d3a6dcbefe5ce13bf4b8b7799fd7a6f290c26b27874ac166c89d501d
data/Gemfile CHANGED
@@ -6,3 +6,4 @@ gem 'jruby-openssl', :platform => :jruby
6
6
  gem 'activerecord-jdbcsqlite3-adapter', :platform => :jruby
7
7
 
8
8
  gem 'pry', :platform => :ruby
9
+ gem 'pry-byebug', :platform => :ruby
data/LICENSE CHANGED
@@ -3,7 +3,7 @@ LICENSE
3
3
 
4
4
  The MIT License
5
5
 
6
- Copyright (c) 2008-2013 Jon Yurek and thoughtbot, inc.
6
+ Copyright (c) 2008-2014 Jon Yurek and thoughtbot, inc.
7
7
 
8
8
  Permission is hereby granted, free of charge, to any person obtaining a copy
9
9
  of this software and associated documentation files (the "Software"), to deal
@@ -22,5 +22,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
22
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
23
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
24
  THE SOFTWARE.
25
-
26
-
data/NEWS CHANGED
@@ -1,28 +1,32 @@
1
- New in 3.5.4:
1
+ New in 4.0.0:
2
2
 
3
- Bug Fix: Users were reporting IOErrors. Stop closing extra files to prevent.
3
+ * Security: Attachments are checked to make sure they're not pulling a fast one.
4
+ * Security: It is now *enforced* that every attachment has a file/mime validation.
5
+ * Bug Fix: Removed a call to IOAdapter#close that was causing issues.
6
+ * Improvement: Added bullets to the 3.5.3 list of changes. Very important.
7
+ * Improcement: Updated the copyright to 2014
4
8
 
5
9
  New in 3.5.3:
6
10
 
7
- Improvement: After three long, hard years... we know how to upgrade
8
- Bug Fix: #expiring_url returns 'missing' urls if nothing is attached
9
- Improvement: Lots of documentation fixes
10
- Improvement: Lots of fixes for Ruby warnings
11
- Improvement: Test the most appropriate Ruby/Rails comobinations on Travis
12
- Improvement: Delegate more IO methods through IOAdapters
13
- Improvement: Remove Rails 4 deprecations
14
- Improvement: Both S3's and Fog's #expiring_url can take a Time or Int
15
- Bug Fix: Both S3's and Fog's expiring_url respect style when missing the file
16
- Bug Fix: Timefiles will have a reasonable-length name. They're all MD5 hashes now
17
- Bug Fix: Don't delete files off S3 when reprocessing due to AWS inconsistencies
18
- Bug Fix: "swallow_stream" isn't thread dafe. Use :swallow_stderr
19
- Improvement: Regexps use \A and \Z instead of ^ and $
20
- Improvement: :s3_credentials can take a lambda as an argument
21
- Improvement: Search up the class heirarchy for attachments
22
- Improvement: deep_merge options instead of regular merge
23
- Bug Fix: Prevent file deletion on transaction rollback
24
- Test Improvement: Ensure more files are properly closed during tests
25
- Test Bug Fix: Return the gemfile's syntax to normal
11
+ * Improvement: After three long, hard years... we know how to upgrade
12
+ * Bug Fix: #expiring_url returns 'missing' urls if nothing is attached
13
+ * Improvement: Lots of documentation fixes
14
+ * Improvement: Lots of fixes for Ruby warnings
15
+ * Improvement: Test the most appropriate Ruby/Rails comobinations on Travis
16
+ * Improvement: Delegate more IO methods through IOAdapters
17
+ * Improvement: Remove Rails 4 deprecations
18
+ * Improvement: Both S3's and Fog's #expiring_url can take a Time or Int
19
+ * Bug Fix: Both S3's and Fog's expiring_url respect style when missing the file
20
+ * Bug Fix: Timefiles will have a reasonable-length name. They're all MD5 hashes now
21
+ * Bug Fix: Don't delete files off S3 when reprocessing due to AWS inconsistencies
22
+ * Bug Fix: "swallow_stream" isn't thread dafe. Use :swallow_stderr
23
+ * Improvement: Regexps use \A and \Z instead of ^ and $
24
+ * Improvement: :s3_credentials can take a lambda as an argument
25
+ * Improvement: Search up the class heirarchy for attachments
26
+ * Improvement: deep_merge options instead of regular merge
27
+ * Bug Fix: Prevent file deletion on transaction rollback
28
+ * Test Improvement: Ensure more files are properly closed during tests
29
+ * Test Bug Fix: Return the gemfile's syntax to normal
26
30
 
27
31
  New in 3.5.2:
28
32
 
data/README.md CHANGED
@@ -104,6 +104,7 @@ Quick Start
104
104
  class User < ActiveRecord::Base
105
105
  attr_accessible :avatar
106
106
  has_attached_file :avatar, :styles => { :medium => "300x300>", :thumb => "100x100>" }, :default_url => "/images/:style/missing.png"
107
+ validates_attachment_content_type :avatar, :content_type => /\Aimage\/.*\Z/
107
108
  end
108
109
  ```
109
110
 
@@ -112,6 +113,7 @@ end
112
113
  ```ruby
113
114
  class User < ActiveRecord::Base
114
115
  has_attached_file :avatar, :styles => { :medium => "300x300>", :thumb => "100x100>" }, :default_url => "/images/:style/missing.png"
116
+ validates_attachment_content_type :avatar, :content_type => /\Aimage\/.*\Z/
115
117
  end
116
118
  ```
117
119
 
@@ -302,6 +304,38 @@ validates_attachment :avatar,
302
304
  `Paperclip::ContentTypeDetector` will attempt to match a file's extension to an
303
305
  inferred content_type, regardless of the actual contents of the file.
304
306
 
307
+ Security Validations
308
+ ====================
309
+
310
+ NOTE: Starting at version 4.0.0, all attachments are *required* to include a
311
+ content_type validation, a file_name validation, or to explicitly state that
312
+ they're not going to have either. *Paperclip will raise an error* if you do not
313
+ do this.
314
+
315
+ ```ruby
316
+ class ActiveRecord::Base
317
+ has_attached_file :avatar
318
+ # Validate content type
319
+ validates_attachment_content_type :avatar, :content_type => /\Aimage/
320
+ # Validate filename
321
+ validates_attachment_file_name :avatar, :matches => [/png\Z/, /jpe?g\Z/]
322
+ # Explicitly do not validate
323
+ do_not_validate_attachment_file_type :avatar
324
+ end
325
+ ```
326
+
327
+ This keeps Paperclip secure-by-default, and will prevent people trying to mess
328
+ with your filesystem.
329
+
330
+ NOTE: Also starting at version 4.0.0, Paperclip has another validation that
331
+ cannot be turned off. This validation will prevent content type spoofing. That
332
+ is, uploading, say, a PHP document as part of the EXIF tags of a well-formed
333
+ JPEG. This check is limited to the media type (the first part of the MIME type,
334
+ so, 'text' in 'text/plain'). This will prevent HTML documents from being
335
+ uploaded as JPEGs, but will not prevent GIFs from being uploaded with a .jpg
336
+ extension. This validation will only add validation errors to the form. It will
337
+ not cause Errors to be raised.
338
+
305
339
  Defaults
306
340
  --------
307
341
  Global defaults for all your paperclip attachments can be defined by changing the Paperclip::Attachment.default_options Hash, this can be useful for setting your default storage settings per example so you won't have to define them in every has_attached_file definition.
@@ -754,5 +788,5 @@ The names and logos for thoughtbot are trademarks of thoughtbot, inc.
754
788
  License
755
789
  -------
756
790
 
757
- Paperclip is Copyright © 2008-2013 thoughtbot, inc. It is free software, and may be
791
+ Paperclip is Copyright © 2008-2014 thoughtbot, inc. It is free software, and may be
758
792
  redistributed under the terms specified in the MIT-LICENSE file.
@@ -11,8 +11,10 @@ World(AttachmentHelpers)
11
11
 
12
12
  When /^I modify my attachment definition to:$/ do |definition|
13
13
  content = in_current_dir { File.read("app/models/user.rb") }
14
+ name = content[/has_attached_file :\w+/][/:\w+/]
14
15
  content.gsub!(/has_attached_file.+end/m, <<-FILE)
15
16
  #{definition}
17
+ do_not_validate_attachment_file_type #{name}
16
18
  end
17
19
  FILE
18
20
 
@@ -69,6 +69,7 @@ def attach_attachment(name, definition = nil)
69
69
  snippet += ", \n"
70
70
  snippet += definition
71
71
  end
72
+ snippet += "\ndo_not_validate_attachment_file_type :#{name}\n"
72
73
  in_current_dir do
73
74
  transform_file("app/models/user.rb") do |content|
74
75
  content.sub(/end\Z/, "#{snippet}\nend")
@@ -43,6 +43,7 @@ require 'paperclip/attachment'
43
43
  require 'paperclip/storage'
44
44
  require 'paperclip/callbacks'
45
45
  require 'paperclip/file_command_content_type_detector'
46
+ require 'paperclip/media_type_spoof_detector'
46
47
  require 'paperclip/content_type_detector'
47
48
  require 'paperclip/glue'
48
49
  require 'paperclip/errors'
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
2
  require 'uri'
3
3
  require 'paperclip/url_generator'
4
+ require 'active_support/deprecation'
4
5
 
5
6
  module Paperclip
6
7
  # The Attachment class manages the files for a given attachment. It saves
@@ -93,6 +94,7 @@ module Paperclip
93
94
  # new_user.avatar = old_user.avatar
94
95
  def assign uploaded_file
95
96
  ensure_required_accessors!
97
+ ensure_required_validations!
96
98
  file = Paperclip.io_adapters.for(uploaded_file)
97
99
 
98
100
  return nil if not file.assignment?
@@ -166,6 +168,18 @@ module Paperclip
166
168
  path.respond_to?(:unescape) ? path.unescape : path
167
169
  end
168
170
 
171
+ # :nodoc:
172
+ def staged_path(style_name = default_style)
173
+ if staged?
174
+ @queued_for_write[style_name].path
175
+ end
176
+ end
177
+
178
+ # :nodoc:
179
+ def staged?
180
+ ! @queued_for_write.empty?
181
+ end
182
+
169
183
  # Alias to +url+
170
184
  def to_s style_name = default_style
171
185
  url(style_name)
@@ -373,6 +387,16 @@ module Paperclip
373
387
  @options[:path].respond_to?(:call) ? @options[:path].call(self) : @options[:path]
374
388
  end
375
389
 
390
+ def active_validator_classes
391
+ @instance.class.validators.map(&:class)
392
+ end
393
+
394
+ def ensure_required_validations!
395
+ if (active_validator_classes & Paperclip::REQUIRED_VALIDATORS).empty?
396
+ raise Paperclip::Errors::MissingRequiredValidatorError
397
+ end
398
+ end
399
+
376
400
  def ensure_required_accessors! #:nodoc:
377
401
  %w(file_name).each do |field|
378
402
  unless @instance.respond_to?("#{name}_#{field}") && @instance.respond_to?("#{name}_#{field}=")
@@ -482,7 +506,7 @@ module Paperclip
482
506
  end
483
507
  end
484
508
 
485
- # called by storage after the writes are flushed and before @queued_for_writes is cleared
509
+ # called by storage after the writes are flushed and before @queued_for_write is cleared
486
510
  def after_flush_writes
487
511
  @queued_for_write.each do |style, file|
488
512
  file.close unless file.closed?
@@ -7,7 +7,7 @@ module Paperclip
7
7
 
8
8
  module Defining
9
9
  def define_paperclip_callbacks(*callbacks)
10
- define_callbacks *[callbacks, {:terminator => "result == false"}].flatten
10
+ define_callbacks(*[callbacks, {:terminator => "result == false"}].flatten)
11
11
  callbacks.each do |callback|
12
12
  eval <<-end_callbacks
13
13
  def before_#{callback}(*args, &blk)
@@ -32,10 +32,6 @@ module Paperclip
32
32
  EMPTY_TYPE
33
33
  elsif calculated_type_matches.any?
34
34
  calculated_type_matches.first
35
- elsif official_type_matches.any?
36
- official_type_matches.first
37
- elsif unofficial_type_matches.any?
38
- unofficial_type_matches.first
39
35
  else
40
36
  type_from_file_command || SENSIBLE_DEFAULT
41
37
  end.to_s
@@ -46,7 +42,7 @@ module Paperclip
46
42
  def empty_file?
47
43
  File.exists?(@filename) && File.size(@filename) == 0
48
44
  end
49
-
45
+
50
46
  alias :empty? :empty_file?
51
47
 
52
48
  def blank_name?
@@ -61,14 +57,6 @@ module Paperclip
61
57
  possible_types.select{|content_type| content_type == type_from_file_command }
62
58
  end
63
59
 
64
- def official_type_matches
65
- possible_types.reject{|content_type| content_type.match(/\/x-/) }
66
- end
67
-
68
- def unofficial_type_matches
69
- possible_types.select{|content_type| content_type.match(/\/x-/) }
70
- end
71
-
72
60
  def type_from_file_command
73
61
  @type_from_file_command ||= FileCommandContentTypeDetector.new(@filename).detect
74
62
  end
@@ -13,6 +13,11 @@ module Paperclip
13
13
  class CommandNotFoundError < Paperclip::Error
14
14
  end
15
15
 
16
+ # Attachments require a content_type or file_name validator,
17
+ # or to have explicitly opted out of them.
18
+ class MissingRequiredValidatorError < Paperclip::Error
19
+ end
20
+
16
21
  # Will be thrown when ImageMagic cannot determine the uploaded file's
17
22
  # metadata, usually this would mean the file is not an image.
18
23
  class NotIdentifiedByImageMagickError < Paperclip::Error
@@ -18,6 +18,7 @@ module Paperclip
18
18
  register_new_attachment
19
19
  add_active_record_callbacks
20
20
  add_paperclip_callbacks
21
+ add_required_validations
21
22
  end
22
23
 
23
24
  private
@@ -77,6 +78,10 @@ module Paperclip
77
78
  Paperclip::AttachmentRegistry.register(@klass, @name, @options)
78
79
  end
79
80
 
81
+ def add_required_validations
82
+ @klass.validates_media_type_spoof_detection @name
83
+ end
84
+
80
85
  def add_active_record_callbacks
81
86
  name = @name
82
87
  @klass.send(:after_save) { send(name).send(:save) }
@@ -34,7 +34,7 @@ module Paperclip
34
34
  private
35
35
 
36
36
  def destination
37
- @destination ||= TempfileFactory.new.generate(original_filename)
37
+ @destination ||= TempfileFactory.new.generate
38
38
  end
39
39
 
40
40
  def copy_to_tempfile(src)
@@ -20,11 +20,11 @@ module Paperclip
20
20
  @size = @tempfile.size || @target.size
21
21
  end
22
22
 
23
- def copy_to_tempfile(src)
24
- if src.respond_to? :copy_to_local_file
25
- src.copy_to_local_file(@style, destination.path)
23
+ def copy_to_tempfile(source)
24
+ if source.staged?
25
+ FileUtils.cp(source.staged_path(@style), destination.path)
26
26
  else
27
- FileUtils.cp(src.path(@style), destination.path)
27
+ source.copy_to_local_file(@style, destination.path)
28
28
  end
29
29
  destination
30
30
  end
@@ -4,19 +4,14 @@ module Paperclip
4
4
  REGEXP = /\Adata:([-\w]+\/[-\w\+]+);base64,(.*)/m
5
5
 
6
6
  def initialize(target_uri)
7
- @target_uri = target_uri
8
- cache_current_values
9
- @tempfile = copy_to_tempfile
7
+ super(extract_target(target_uri))
10
8
  end
11
9
 
12
10
  private
13
11
 
14
- def cache_current_values
15
- self.original_filename = 'base64.txt'
16
- data_uri_parts ||= @target_uri.match(REGEXP) || []
17
- @content_type = data_uri_parts[1] || 'text/plain'
18
- @target = StringIO.new(Base64.decode64(data_uri_parts[2] || ''))
19
- @size = @target.size
12
+ def extract_target(uri)
13
+ data_uri_parts = uri.match(REGEXP) || []
14
+ StringIO.new(Base64.decode64(data_uri_parts[2] || ''))
20
15
  end
21
16
 
22
17
  end
@@ -2,8 +2,8 @@ module Paperclip
2
2
  class StringioAdapter < AbstractAdapter
3
3
  def initialize(target)
4
4
  @target = target
5
- cache_current_values
6
5
  @tempfile = copy_to_tempfile
6
+ cache_current_values
7
7
  end
8
8
 
9
9
  attr_writer :content_type
@@ -11,13 +11,10 @@ module Paperclip
11
11
  private
12
12
 
13
13
  def cache_current_values
14
- @original_filename = @target.original_filename if @target.respond_to?(:original_filename)
15
- @original_filename ||= "stringio.txt"
16
- self.original_filename = @original_filename.strip
17
-
18
- @content_type = @target.content_type if @target.respond_to?(:content_type)
19
- @content_type ||= "text/plain"
20
-
14
+ @content_type = ContentTypeDetector.new(@tempfile.path).detect
15
+ original_filename = @target.original_filename if @target.respond_to?(:original_filename)
16
+ original_filename ||= "data.#{extension_for(@content_type)}"
17
+ self.original_filename = original_filename.strip
21
18
  @size = @target.size
22
19
  end
23
20
 
@@ -29,6 +26,11 @@ module Paperclip
29
26
  destination
30
27
  end
31
28
 
29
+ def extension_for(content_type)
30
+ type = MIME::Types[content_type].first
31
+ type && type.extensions.first
32
+ end
33
+
32
34
  end
33
35
  end
34
36
 
@@ -0,0 +1,36 @@
1
+ module Paperclip
2
+ class MediaTypeSpoofDetector
3
+ def self.using(file, name)
4
+ new(file, name)
5
+ end
6
+
7
+ def initialize(file, name)
8
+ @file = file
9
+ @name = name
10
+ end
11
+
12
+ def spoofed?
13
+ if ! @name.blank?
14
+ ! supplied_file_media_type.include?(calculated_media_type)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def supplied_file_media_type
21
+ MIME::Types.type_for(@name).collect(&:media_type)
22
+ end
23
+
24
+ def calculated_media_type
25
+ type_from_file_command.split("/").first
26
+ end
27
+
28
+ def type_from_file_command
29
+ begin
30
+ Paperclip.run("file", "-b --mime-type :file", :file => @file.path)
31
+ rescue Cocaine::CommandLineError
32
+ ""
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,7 +1,7 @@
1
1
  module Paperclip
2
2
  class TempfileFactory
3
3
 
4
- def generate(name)
4
+ def generate(name = random_name)
5
5
  @name = name
6
6
  file = Tempfile.new([basename, extension])
7
7
  file.binmode
@@ -15,5 +15,9 @@ module Paperclip
15
15
  def basename
16
16
  Digest::MD5.hexdigest(File.basename(@name, extension))
17
17
  end
18
+
19
+ def random_name
20
+ SecureRandom.uuid
21
+ end
18
22
  end
19
23
  end