shrine 1.0.0 → 1.1.0

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

Potentially problematic release.


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

Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -149
  3. data/doc/carrierwave.md +12 -16
  4. data/doc/changing_location.md +50 -0
  5. data/doc/creating_plugins.md +2 -2
  6. data/doc/creating_storages.md +70 -9
  7. data/doc/direct_s3.md +132 -61
  8. data/doc/migrating_storage.md +12 -10
  9. data/doc/paperclip.md +12 -17
  10. data/doc/refile.md +338 -0
  11. data/doc/regenerating_versions.md +75 -11
  12. data/doc/securing_uploads.md +172 -0
  13. data/lib/shrine.rb +21 -16
  14. data/lib/shrine/plugins/activerecord.rb +2 -2
  15. data/lib/shrine/plugins/background_helpers.rb +2 -148
  16. data/lib/shrine/plugins/backgrounding.rb +148 -0
  17. data/lib/shrine/plugins/backup.rb +88 -0
  18. data/lib/shrine/plugins/data_uri.rb +25 -4
  19. data/lib/shrine/plugins/default_url.rb +37 -0
  20. data/lib/shrine/plugins/delete_uploaded.rb +40 -0
  21. data/lib/shrine/plugins/determine_mime_type.rb +4 -2
  22. data/lib/shrine/plugins/direct_upload.rb +107 -62
  23. data/lib/shrine/plugins/download_endpoint.rb +157 -0
  24. data/lib/shrine/plugins/hooks.rb +19 -5
  25. data/lib/shrine/plugins/keep_location.rb +43 -0
  26. data/lib/shrine/plugins/moving.rb +11 -10
  27. data/lib/shrine/plugins/parallelize.rb +1 -5
  28. data/lib/shrine/plugins/parsed_json.rb +7 -1
  29. data/lib/shrine/plugins/pretty_location.rb +6 -0
  30. data/lib/shrine/plugins/rack_file.rb +7 -1
  31. data/lib/shrine/plugins/remove_invalid.rb +22 -0
  32. data/lib/shrine/plugins/sequel.rb +2 -2
  33. data/lib/shrine/plugins/upload_options.rb +41 -0
  34. data/lib/shrine/plugins/versions.rb +9 -7
  35. data/lib/shrine/storage/file_system.rb +46 -30
  36. data/lib/shrine/storage/linter.rb +48 -25
  37. data/lib/shrine/storage/s3.rb +89 -22
  38. data/lib/shrine/version.rb +1 -1
  39. data/shrine.gemspec +3 -3
  40. metadata +16 -5
@@ -0,0 +1,172 @@
1
+ # Securing uploads
2
+
3
+ Shrine does a lot to make your file uploads secure, but there are still a lot
4
+ of security measures that could be added by the user on the application's side.
5
+ This guide will try to cover all the well-known security issues, ranging from
6
+ the obvious ones to not-so-obvious ones, and try to provide solutions.
7
+
8
+ ## Validate file type
9
+
10
+ Almost always you will be accepting certain types of files, and it's a good
11
+ idea to create a whitelist (or blaclist) of supported extensions and MIME
12
+ types.
13
+
14
+ By default Shrine stores the MIME type derived from the extension, which means
15
+ it's not guaranteed to hold the actual MIME type of the the file. However, you
16
+ can load the `determine_mime_type` plugin which by default uses the [file]
17
+ utility to determine the MIME type from magic file headers.
18
+
19
+ ```rb
20
+ class MyUploader < Shrine
21
+ plugin :validation_helpers
22
+ plugin :determine_mime_type
23
+
24
+ Attacher.validate do
25
+ validate_extension_inclusion [/jpe?g/, "png", "gif"]
26
+ validate_mime_type_inclusion ["image/jpeg", "image/png", "image/gif"]
27
+ end
28
+ end
29
+ ```
30
+
31
+ ## Limit filesize
32
+
33
+ It's a good idea to generally limit the filesize of uploaded files, so that
34
+ attackers cannot easily flood your storage. There are various layers at which
35
+ you can apply filesize limits, depending on how you're accepting uploads.
36
+ Firstly, you should probably add a filesize validation to prevent large files
37
+ from being uploaded to `:store`:
38
+
39
+ ```rb
40
+ class MyUploader < Shrine
41
+ plugin :validation_helpers
42
+
43
+ Attacher.validate do
44
+ validate_max_size 20*1024*1024 # 20 MB
45
+ end
46
+ end
47
+ ```
48
+
49
+ In the following sections we talk about various strategies to prevent files from
50
+ being uploaded to cache and the temporary directory.
51
+
52
+ ### Direct uploads
53
+
54
+ If you're doing direct uploads with the `direct_upload` plugin, you can pass
55
+ in the `:max_size` option, which will refuse too large files and automatically
56
+ delete it from temporary storage.
57
+
58
+ ```rb
59
+ plugin :direct_upload, max_size: 20*1024*1024 # 20 MB
60
+ ```
61
+
62
+ This option doesn't apply to presigned uploads, if you're using S3 you can
63
+ limit the filesize on presigning:
64
+
65
+ ```rb
66
+ plugin :direct_upload, presign: ->(request) do
67
+ {content_length_range: 0..20*1024*1024}
68
+ end
69
+ ```
70
+
71
+ ### Regular uploads
72
+
73
+ If you're simply accepting uploads synchronously in the form, you can prevent
74
+ large files from getting into cache by loading the `remove_invalid` plugin:
75
+
76
+ ```rb
77
+ plugin :remove_invalid
78
+ ```
79
+
80
+ ### Limiting at application level
81
+
82
+ If your application is accepting file uploads directly (either through direct
83
+ uploads or regular ones), you can limit the maximum request body size in your
84
+ application server (nginx or apache):
85
+
86
+ ```sh
87
+ # nginx.conf
88
+
89
+ http {
90
+ # ...
91
+ server {
92
+ # ...
93
+ client_max_body_size 20M;
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### Paranoid limiting
99
+
100
+ If you want to make sure that no large files ever get to your storages, and
101
+ you don't really care about the error message, you can use the `hooks` plugin
102
+ and raise an error:
103
+
104
+ ```rb
105
+ class MyUploader
106
+ plugin :hooks
107
+
108
+ def before_upload(io, context)
109
+ if io.respond_to?(:read)
110
+ raise FileTooLarge if io.size >= 20*1024*1024
111
+ end
112
+ end
113
+ end
114
+ ```
115
+
116
+ ## Limit image dimensions
117
+
118
+ It's possible to create so-called [image bombs], which are images that have a
119
+ small filesize but very large dimensions. These are dangerous if you're doing
120
+ image processing, since processing them can take a lot of time and memory. This
121
+ makes it trivial to DoS the application which doesn't have any protection
122
+ against them.
123
+
124
+ Shrine uses the [fastimage] gem for determining image dimensions which has
125
+ built-in protection against image bombs (ImageMagick for example doesn't), but
126
+ you still need to prevent those files from being attached and processed:
127
+
128
+ ```rb
129
+ class MyUploader < Shrine
130
+ plugin :store_dimensions
131
+ plugin :validation_helpers
132
+
133
+ Attacher.validate do
134
+ validate_max_width 2500
135
+ validate_max_height 2500
136
+ end
137
+ end
138
+ ```
139
+
140
+ If you're doing processing on caching, you can use the fastimage gem directly
141
+ in a conditional.
142
+
143
+ ## Limit number of files
144
+
145
+ When doing direct uploads, it's a good idea to apply some kind of throttling to
146
+ the endpoint, to ensure the attacker cannot upload an unlimited number files,
147
+ because even with a filesize limit it would allow flooding the storage. A good
148
+ library for throttling requests is [rack-attack].
149
+
150
+ Also, it's generally a good idea to limit the *minimum* filesize as well as
151
+ maximum, to prevent uploading large amounts of small files:
152
+
153
+ ```rb
154
+ class MyUploader < Shrine
155
+ plugin :validation_helpers
156
+
157
+ Attacher.validate do
158
+ validate_min_size 10*1024 # 10 KB
159
+ end
160
+ end
161
+ ```
162
+
163
+ ## References
164
+
165
+ * [Nvisium: Secure File Uploads](https://nvisium.com/blog/2015/10/13/secure-file-uploads/)
166
+ * [OWASP: Unrestricted File Upload](https://www.owasp.org/index.php/Unrestricted_File_Upload)
167
+ * [AppSec: 8 Basic Rules to Implement Secure File Uploads](https://software-security.sans.org/blog/2009/12/28/8-basic-rules-to-implement-secure-file-uploads/)
168
+
169
+ [image bombs]: https://www.bamsoftware.com/hacks/deflate.html
170
+ [fastimage]: https://github.com/sdsykes/fastimage
171
+ [file]: http://linux.die.net/man/1/file
172
+ [rack-attack]: https://github.com/kickstarter/rack-attack
data/lib/shrine.rb CHANGED
@@ -207,7 +207,8 @@ class Shrine
207
207
 
208
208
  # The main method for uploading files. Takes in an IO object and an
209
209
  # optional context (used internally by Shrine::Attacher). It calls
210
- # user-defined #process, and aferwards it calls #store.
210
+ # user-defined #process, and aferwards it calls #store. The `io` is
211
+ # closed after upload.
211
212
  def upload(io, context = {})
212
213
  io = processed(io, context) || io
213
214
  store(io, context)
@@ -309,15 +310,15 @@ class Shrine
309
310
  # the metadata, stores the file, and returns a Shrine::UploadedFile.
310
311
  def _store(io, context)
311
312
  _enforce_io(io)
312
- location = context[:location] || generate_location(io, context)
313
- metadata = extract_metadata(io, context)
313
+ context[:location] ||= get_location(io, context)
314
+ context[:metadata] ||= extract_metadata(io, context)
314
315
 
315
- put(io, context.merge(location: location, metadata: metadata))
316
+ put(io, context)
316
317
 
317
318
  self.class::UploadedFile.new(
318
- "id" => location,
319
+ "id" => context[:location],
319
320
  "storage" => storage_key.to_s,
320
- "metadata" => metadata,
321
+ "metadata" => context[:metadata],
321
322
  )
322
323
  end
323
324
 
@@ -334,12 +335,7 @@ class Shrine
334
335
  # Does the actual uploading, calling `#upload` on the storage.
335
336
  def copy(io, context)
336
337
  storage.upload(io, context[:location], context[:metadata])
337
- end
338
-
339
- # Some storages support moving, so we provide this method for plugins
340
- # to use, but by default the file will be copied.
341
- def move(io, context)
342
- storage.move(io, context[:location], context[:metadata])
338
+ io.close rescue nil
343
339
  end
344
340
 
345
341
  # Does the actual deletion, calls `UploadedFile#delete`.
@@ -352,6 +348,12 @@ class Shrine
352
348
  process(io, context)
353
349
  end
354
350
 
351
+ # Retrieves the location for the given io and context. First it looks
352
+ # for the `:location` option, otherwise it calls #generate_location.
353
+ def get_location(io, context)
354
+ generate_location(io, context)
355
+ end
356
+
355
357
  # Checks if the object is a valid IO by checking that it responds to
356
358
  # `#read`, `#eof?`, `#rewind`, `#size` and `#close`, otherwise raises
357
359
  # Shrine::InvalidFile.
@@ -598,7 +600,9 @@ class Shrine
598
600
 
599
601
  # Delegates to `Shrine#default_url`.
600
602
  def default_url(**options)
601
- store.default_url(options.merge(context))
603
+ url = store.default_url(options.merge(context))
604
+ warn "Overriding Shrine#default_url is deprecated and will be removed in Shrine 2. You should use the default_url plugin." if url
605
+ url
602
606
  end
603
607
 
604
608
  # The validation block provided by `Shrine.validate`.
@@ -630,7 +634,7 @@ class Shrine
630
634
  # The context that's sent to Shrine on upload and delete. It holds the
631
635
  # record and the name of the attachment.
632
636
  def context
633
- @context ||= {name: name, record: record}.freeze
637
+ @context ||= {name: name, record: record}
634
638
  end
635
639
  end
636
640
 
@@ -676,8 +680,7 @@ class Shrine
676
680
 
677
681
  # The extension derived from `#original_filename`.
678
682
  def extension
679
- extname = File.extname(id)
680
- extname[1..-1] unless extname.empty?
683
+ File.extname(id)[1..-1] || File.extname(original_filename.to_s)[1..-1]
681
684
  end
682
685
 
683
686
  # The filesize of the original file.
@@ -689,6 +692,7 @@ class Shrine
689
692
  def mime_type
690
693
  metadata.fetch("mime_type")
691
694
  end
695
+ alias content_type mime_type
692
696
 
693
697
  # Part of Shrine::UploadedFile's complying to the IO interface. It
694
698
  # delegates to the internally downloaded file.
@@ -706,6 +710,7 @@ class Shrine
706
710
  # delegates to the internally downloaded file.
707
711
  def close
708
712
  io.close
713
+ io.delete if io.class.name == "Tempfile"
709
714
  end
710
715
 
711
716
  # Part of Shrine::UploadedFile's complying to the IO interface. It
@@ -26,7 +26,7 @@ class Shrine
26
26
  # you should first disable these transactions for those tests.
27
27
  #
28
28
  # If you want to put some parts of this lifecycle into a background job, see
29
- # the background_helpers plugin.
29
+ # the backgrounding plugin.
30
30
  #
31
31
  # Additionally, any Shrine validation errors will added to ActiveRecord's
32
32
  # errors upon validation. Note that if you want to validate presence of the
@@ -64,7 +64,7 @@ class Shrine
64
64
  end
65
65
 
66
66
  module AttacherClassMethods
67
- # Needed by the background_helpers plugin.
67
+ # Needed by the backgrounding plugin.
68
68
  def find_record(record_class, record_id)
69
69
  record_class.find(record_id)
70
70
  end
@@ -1,148 +1,2 @@
1
- class Shrine
2
- module Plugins
3
- # The background_helpers plugin enables you to intercept phases of
4
- # uploading and put them into background jobs. This doesn't require any
5
- # additional columns.
6
- #
7
- # plugin :background_helpers
8
- #
9
- # ## Promoting
10
- #
11
- # If you're doing processing, or your `:store` is something other than
12
- # Storage::FileSystem, it's recommended to put promoting (moving to store)
13
- # into a background job. This plugin allows you to do that by calling
14
- # `Shrine::Attacher.promote`:
15
- #
16
- # Shrine::Attacher.promote { |data| UploadJob.perform_async(data) }
17
- #
18
- # When you call `Shrine::Attacher.promote` with a block, it will save the
19
- # block and call it on every promotion. Then in your background job you can
20
- # again call `Shrine::Attacher.promote` with the data, and internally it
21
- # will resolve all necessary objects, do the promoting and update the
22
- # record.
23
- #
24
- # class UploadJob
25
- # include Sidekiq::Worker
26
- #
27
- # def perform(data)
28
- # Shrine::Attacher.promote(data)
29
- # end
30
- # end
31
- #
32
- # Shrine automatically handles all concurrency issues, such as canceling
33
- # promoting if the attachment has changed in the meanwhile.
34
- #
35
- # ## Deleting
36
- #
37
- # If your `:store` is something other than Storage::FileSystem, it's
38
- # recommended to put deleting files into a background job. This plugin
39
- # allows you to do that by calling `Shrine::Attacher.delete`:
40
- #
41
- # Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
42
- #
43
- # When you call `Shrine::Attacher.delete` with a block, it will save the
44
- # block and call it on every delete. Then in your background job you can
45
- # again call `Shrine::Attacher.delete` with the data, and internally it
46
- # will resolve all necessary objects, and delete the file.
47
- #
48
- # class DeleteJob
49
- # include Sidekiq::Worker
50
- #
51
- # def perform(data)
52
- # Shrine::Attacher.delete(data)
53
- # end
54
- # end
55
- #
56
- # ## Conclusion
57
- #
58
- # The examples above used Sidekiq, but obviously you can just as well use
59
- # any other backgrounding library. Also, if you want you can use
60
- # backgrounding just for certain uploaders:
61
- #
62
- # class ImageUploader < Shrine
63
- # Attacher.promote { |data| UploadJob.perform_async(data) }
64
- # Attacher.delete { |data| DeleteJob.perform_async(data) }
65
- # end
66
- #
67
- # If you would like to speed up your uploads and deletes, you can use the
68
- # parallelize plugin, either as a replacement or an addition to
69
- # background_helpers.
70
- module BackgroundHelpers
71
- module AttacherClassMethods
72
- # If block is passed in, stores it to be called on promotion. Otherwise
73
- # resolves data into objects and calls Attacher#promote.
74
- def promote(data = nil, &block)
75
- if block
76
- shrine_class.opts[:background_promote] = block
77
- else
78
- record_class, record_id = data["record"]
79
- record_class = Object.const_get(record_class)
80
- record = find_record(record_class, record_id)
81
-
82
- name = data["attachment"]
83
- attacher = record.send("#{name}_attacher")
84
- cached_file = attacher.uploaded_file(data["uploaded_file"])
85
-
86
- attacher.promote(cached_file)
87
- end
88
- end
89
-
90
- # If block is passed in, stores it to be called on deletion. Otherwise
91
- # resolves data into objects and calls `Shrine#delete`.
92
- def delete(data = nil, &block)
93
- if block
94
- shrine_class.opts[:background_delete] = block
95
- else
96
- record_class, record_id = data["record"]
97
- record = Object.const_get(record_class).new
98
- record.id = record_id
99
-
100
- name, phase = data["attachment"], data["phase"]
101
- attacher = record.send("#{name}_attacher")
102
- uploaded_file = attacher.uploaded_file(data["uploaded_file"])
103
- context = {name: name.to_sym, record: record, phase: phase.to_sym}
104
-
105
- attacher.store.delete(uploaded_file, context)
106
- end
107
- end
108
- end
109
-
110
- module AttacherMethods
111
- # Calls the promoting block with the data if it's been registered.
112
- def _promote
113
- if background_promote = shrine_class.opts[:background_promote]
114
- data = {
115
- "uploaded_file" => get.to_json,
116
- "record" => [record.class.to_s, record.id],
117
- "attachment" => name,
118
- }
119
-
120
- instance_exec(data, &background_promote) if promote?(get)
121
- else
122
- super
123
- end
124
- end
125
-
126
- private
127
-
128
- # Calls the deleting block with the data if it's been registered.
129
- def delete!(uploaded_file, phase:)
130
- if background_delete = shrine_class.opts[:background_delete]
131
- data = {
132
- "uploaded_file" => uploaded_file.to_json,
133
- "record" => [record.class.to_s, record.id],
134
- "attachment" => name,
135
- "phase" => phase,
136
- }
137
-
138
- instance_exec(data, &background_delete)
139
- else
140
- super(uploaded_file, phase: phase)
141
- end
142
- end
143
- end
144
- end
145
-
146
- register_plugin(:background_helpers, BackgroundHelpers)
147
- end
148
- end
1
+ require "shrine/plugins/backgrounding"
2
+ Shrine::Plugins.register_plugin(:background_helpers, Shrine::Plugins::Backgrounding)
@@ -0,0 +1,148 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The background_helpers plugin enables you to intercept phases of
4
+ # uploading and put them into background jobs. This doesn't require any
5
+ # additional columns.
6
+ #
7
+ # plugin :backgrounding
8
+ #
9
+ # ## Promoting
10
+ #
11
+ # If you're doing processing, or your `:store` is something other than
12
+ # Storage::FileSystem, it's recommended to put promoting (moving to store)
13
+ # into a background job. This plugin allows you to do that by calling
14
+ # `Shrine::Attacher.promote`:
15
+ #
16
+ # Shrine::Attacher.promote { |data| UploadJob.perform_async(data) }
17
+ #
18
+ # When you call `Shrine::Attacher.promote` with a block, it will save the
19
+ # block and call it on every promotion. Then in your background job you can
20
+ # again call `Shrine::Attacher.promote` with the data, and internally it
21
+ # will resolve all necessary objects, do the promoting and update the
22
+ # record.
23
+ #
24
+ # class UploadJob
25
+ # include Sidekiq::Worker
26
+ #
27
+ # def perform(data)
28
+ # Shrine::Attacher.promote(data)
29
+ # end
30
+ # end
31
+ #
32
+ # Shrine automatically handles all concurrency issues, such as canceling
33
+ # promoting if the attachment has changed in the meanwhile.
34
+ #
35
+ # ## Deleting
36
+ #
37
+ # If your `:store` is something other than Storage::FileSystem, it's
38
+ # recommended to put deleting files into a background job. This plugin
39
+ # allows you to do that by calling `Shrine::Attacher.delete`:
40
+ #
41
+ # Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
42
+ #
43
+ # When you call `Shrine::Attacher.delete` with a block, it will save the
44
+ # block and call it on every delete. Then in your background job you can
45
+ # again call `Shrine::Attacher.delete` with the data, and internally it
46
+ # will resolve all necessary objects, and delete the file.
47
+ #
48
+ # class DeleteJob
49
+ # include Sidekiq::Worker
50
+ #
51
+ # def perform(data)
52
+ # Shrine::Attacher.delete(data)
53
+ # end
54
+ # end
55
+ #
56
+ # ## Conclusion
57
+ #
58
+ # The examples above used Sidekiq, but obviously you can just as well use
59
+ # any other backgrounding library. Also, if you want you can use
60
+ # backgrounding just for certain uploaders:
61
+ #
62
+ # class ImageUploader < Shrine
63
+ # Attacher.promote { |data| UploadJob.perform_async(data) }
64
+ # Attacher.delete { |data| DeleteJob.perform_async(data) }
65
+ # end
66
+ #
67
+ # If you would like to speed up your uploads and deletes, you can use the
68
+ # parallelize plugin, either as a replacement or an addition to
69
+ # background_helpers.
70
+ module Backgrounding
71
+ module AttacherClassMethods
72
+ # If block is passed in, stores it to be called on promotion. Otherwise
73
+ # resolves data into objects and calls `Attacher#promote`.
74
+ def promote(data = nil, &block)
75
+ if block
76
+ shrine_class.opts[:backgrounding_promote] = block
77
+ else
78
+ record_class, record_id = data["record"]
79
+ record_class = Object.const_get(record_class)
80
+ record = find_record(record_class, record_id)
81
+
82
+ name = data["attachment"]
83
+ attacher = record.send("#{name}_attacher")
84
+ cached_file = attacher.uploaded_file(data["uploaded_file"])
85
+
86
+ attacher.promote(cached_file)
87
+ end
88
+ end
89
+
90
+ # If block is passed in, stores it to be called on deletion. Otherwise
91
+ # resolves data into objects and calls `Shrine#delete`.
92
+ def delete(data = nil, &block)
93
+ if block
94
+ shrine_class.opts[:backgrounding_delete] = block
95
+ else
96
+ record_class, record_id = data["record"]
97
+ record = Object.const_get(record_class).new
98
+ record.id = record_id
99
+
100
+ name, phase = data["attachment"], data["phase"]
101
+ attacher = record.send("#{name}_attacher")
102
+ uploaded_file = attacher.uploaded_file(data["uploaded_file"])
103
+ context = {name: name.to_sym, record: record, phase: phase.to_sym}
104
+
105
+ attacher.store.delete(uploaded_file, context)
106
+ end
107
+ end
108
+ end
109
+
110
+ module AttacherMethods
111
+ # Calls the promoting block with the data if it's been registered.
112
+ def _promote
113
+ if background_promote = shrine_class.opts[:backgrounding_promote]
114
+ data = {
115
+ "uploaded_file" => get.to_json,
116
+ "record" => [record.class.to_s, record.id],
117
+ "attachment" => name.to_s,
118
+ }
119
+
120
+ instance_exec(data, &background_promote) if promote?(get)
121
+ else
122
+ super
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ # Calls the deleting block with the data if it's been registered.
129
+ def delete!(uploaded_file, phase:)
130
+ if background_delete = shrine_class.opts[:backgrounding_delete]
131
+ data = {
132
+ "uploaded_file" => uploaded_file.to_json,
133
+ "record" => [record.class.to_s, record.id],
134
+ "attachment" => name.to_s,
135
+ "phase" => phase.to_s,
136
+ }
137
+
138
+ instance_exec(data, &background_delete)
139
+ else
140
+ super(uploaded_file, phase: phase)
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ register_plugin(:backgrounding, Backgrounding)
147
+ end
148
+ end