shrine 3.0.0.beta2 → 3.0.0.beta3

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -1
  3. data/README.md +100 -106
  4. data/doc/advantages.md +90 -88
  5. data/doc/attacher.md +322 -152
  6. data/doc/carrierwave.md +105 -113
  7. data/doc/changing_derivatives.md +308 -0
  8. data/doc/changing_location.md +92 -21
  9. data/doc/changing_storage.md +107 -0
  10. data/doc/creating_plugins.md +1 -1
  11. data/doc/design.md +8 -9
  12. data/doc/direct_s3.md +3 -2
  13. data/doc/metadata.md +97 -78
  14. data/doc/multiple_files.md +3 -3
  15. data/doc/paperclip.md +89 -88
  16. data/doc/plugins/activerecord.md +3 -12
  17. data/doc/plugins/backgrounding.md +126 -100
  18. data/doc/plugins/derivation_endpoint.md +4 -5
  19. data/doc/plugins/derivatives.md +63 -32
  20. data/doc/plugins/download_endpoint.md +54 -1
  21. data/doc/plugins/entity.md +1 -0
  22. data/doc/plugins/form_assign.md +53 -0
  23. data/doc/plugins/mirroring.md +37 -16
  24. data/doc/plugins/multi_cache.md +22 -0
  25. data/doc/plugins/presign_endpoint.md +1 -1
  26. data/doc/plugins/remote_url.md +19 -4
  27. data/doc/plugins/validation.md +83 -0
  28. data/doc/processing.md +149 -133
  29. data/doc/refile.md +68 -63
  30. data/doc/release_notes/3.0.0.md +835 -0
  31. data/doc/securing_uploads.md +56 -36
  32. data/doc/storage/s3.md +2 -2
  33. data/doc/testing.md +104 -120
  34. data/doc/upgrading_to_3.md +538 -0
  35. data/doc/validation.md +48 -87
  36. data/lib/shrine.rb +7 -4
  37. data/lib/shrine/attacher.rb +16 -6
  38. data/lib/shrine/plugins/activerecord.rb +33 -14
  39. data/lib/shrine/plugins/atomic_helpers.rb +1 -1
  40. data/lib/shrine/plugins/backgrounding.rb +23 -89
  41. data/lib/shrine/plugins/data_uri.rb +13 -2
  42. data/lib/shrine/plugins/derivation_endpoint.rb +7 -11
  43. data/lib/shrine/plugins/derivatives.rb +44 -20
  44. data/lib/shrine/plugins/download_endpoint.rb +26 -0
  45. data/lib/shrine/plugins/form_assign.rb +6 -3
  46. data/lib/shrine/plugins/keep_files.rb +2 -2
  47. data/lib/shrine/plugins/mirroring.rb +62 -22
  48. data/lib/shrine/plugins/model.rb +2 -2
  49. data/lib/shrine/plugins/multi_cache.rb +27 -0
  50. data/lib/shrine/plugins/remote_url.rb +25 -10
  51. data/lib/shrine/plugins/remove_invalid.rb +1 -1
  52. data/lib/shrine/plugins/sequel.rb +39 -20
  53. data/lib/shrine/plugins/validation.rb +3 -0
  54. data/lib/shrine/storage/s3.rb +16 -1
  55. data/lib/shrine/uploaded_file.rb +1 -0
  56. data/lib/shrine/version.rb +1 -1
  57. data/shrine.gemspec +1 -1
  58. metadata +12 -7
  59. data/doc/migrating_storage.md +0 -76
  60. data/doc/regenerating_versions.md +0 -143
  61. data/lib/shrine/plugins/attacher_options.rb +0 -55
@@ -2,8 +2,8 @@
2
2
 
3
3
  Shrine does a lot to make your file uploads secure, but there are still a lot
4
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.
5
+ This guide will try to cover some well-known security issues, ranging from the
6
+ obvious ones to not-so-obvious ones, and try to provide solutions.
7
7
 
8
8
  ## Validate file type
9
9
 
@@ -12,17 +12,21 @@ idea to create a whitelist (or a blacklist) of extensions and MIME types.
12
12
 
13
13
  By default Shrine stores the MIME type derived from the extension, which means
14
14
  it's not guaranteed to hold the actual MIME type of the the file. However, you
15
- can load the `determine_mime_type` plugin which by default uses the [file]
16
- utility to determine the MIME type from magic file headers.
15
+ can load the `determine_mime_type` plugin to determine MIME type from magic
16
+ file headers.
17
17
 
18
+ ```rb
19
+ # Gemfile
20
+ gem "marcel", "~> 0.3"
21
+ ```
18
22
  ```rb
19
23
  class MyUploader < Shrine
24
+ plugin :determine_mime_type, analyzer: :marcel
20
25
  plugin :validation_helpers
21
- plugin :determine_mime_type
22
26
 
23
27
  Attacher.validate do
24
- validate_extension_inclusion %w[jpg jpeg png gif]
25
- validate_mime_type_inclusion %w[image/jpeg image/png image/gif]
28
+ validate_extension %w[jpg jpeg png webp]
29
+ validate_mime_type %w[image/jpeg image/png image/webp]
26
30
  end
27
31
  end
28
32
  ```
@@ -31,22 +35,23 @@ end
31
35
 
32
36
  It's a good idea to generally limit the filesize of uploaded files, so that
33
37
  attackers cannot easily flood your storage. There are various layers at which
34
- you can apply filesize limits, depending on how you're accepting uploads.
35
- Firstly, you should probably add a filesize validation to prevent large files
36
- from being uploaded to `:store`:
38
+ you can apply filesize limits, depending on how you're accepting uploads. For
39
+ starters you can add a filesize validation to prevent large files from being
40
+ uploaded to `:store`:
37
41
 
38
42
  ```rb
39
43
  class MyUploader < Shrine
40
44
  plugin :validation_helpers
41
45
 
42
46
  Attacher.validate do
43
- validate_max_size 20*1024*1024 # 20 MB
47
+ validate_max_size 100*1024*1024 # 100 MB
44
48
  end
45
49
  end
46
50
  ```
47
51
 
48
- In the following sections we talk about various strategies to prevent files from
49
- being uploaded to cache and the temporary directory.
52
+ In the following sections we talk about various strategies to prevent files
53
+ from being uploaded to Shrine's temporary storage and the system's temporary
54
+ directory.
50
55
 
51
56
  ### Limiting filesize in direct uploads
52
57
 
@@ -55,7 +60,7 @@ in the `:max_size` option to reject files that are larger than the specified
55
60
  limit:
56
61
 
57
62
  ```rb
58
- plugin :upload_endpoint, max_size: 20*1024*1024 # 20 MB
63
+ plugin :upload_endpoint, max_size: 100*1024*1024 # 20 MB
59
64
  ```
60
65
 
61
66
  If you're doing direct uploads to Amazon S3 using the `presign_endpoint`
@@ -63,7 +68,7 @@ plugin, you can pass in the `:content_length_range` presign option:
63
68
 
64
69
  ```rb
65
70
  plugin :presign_endpoint, presign_options: -> (request) do
66
- { content_length_range: 0..20*1024*1024 }
71
+ { content_length_range: 0..100*1024*1024 }
67
72
  end
68
73
  ```
69
74
 
@@ -92,20 +97,17 @@ loading the `remove_invalid` plugin.
92
97
  plugin :remove_invalid
93
98
  ```
94
99
 
95
- ### Paranoid filesize limiting
100
+ ### Failsafe filesize limiting
96
101
 
97
- If you want to make sure that no large files ever get to your storages, and
98
- you don't really care about the error message, you can use the `hooks` plugin
99
- and raise an error:
102
+ If you want to make sure that no large files ever get to your storages, and you
103
+ don't really care about the error message, you can override `Shrine#upload`:
100
104
 
101
105
  ```rb
102
- class MyUploader
103
- plugin :hooks
106
+ class MyUploader < Shrine
107
+ def upload(io, **options)
108
+ fail FileTooLarge if io.size >= 100*1024*1024
104
109
 
105
- def before_upload(io, context)
106
- if io.respond_to?(:read)
107
- raise FileTooLarge if io.size >= 20*1024*1024
108
- end
110
+ super
109
111
  end
110
112
  end
111
113
  ```
@@ -118,24 +120,43 @@ image processing, since processing them can take a lot of time and memory. This
118
120
  makes it trivial to DoS the application which doesn't have any protection
119
121
  against them.
120
122
 
121
- Shrine uses the [fastimage] gem for determining image dimensions which has
122
- built-in protection against image bombs (ImageMagick for example doesn't), but
123
- you still need to prevent those files from being attached and processed:
123
+ So, in addition to validating filesize, we should also validate image
124
+ dimensions:
124
125
 
125
126
  ```rb
126
- class MyUploader < Shrine
127
+ # Gemfile
128
+ gem "fastimage"
129
+ ```
130
+ ```rb
131
+ class ImageUploader < Shrine
127
132
  plugin :store_dimensions
128
133
  plugin :validation_helpers
129
134
 
130
135
  Attacher.validate do
131
- validate_max_width 2500
132
- validate_max_height 2500
136
+ validate_max_size 100*1024*1024
137
+
138
+ if validate_mime_type %w[image/jpeg image/png image/webp]
139
+ validate_max_dimensions [5000, 5000]
140
+ end
133
141
  end
134
142
  end
135
143
  ```
136
144
 
137
- If you're doing processing on caching, you can use the fastimage gem directly
138
- in a conditional.
145
+ If you want to be extra safe, you can add a failsafe before performing
146
+ processing:
147
+
148
+ ```rb
149
+ class ImageUploader < Shrine
150
+ # ...
151
+ Attacher.derivatives_processor do |original|
152
+ width, height = Shrine.dimensions(original)
153
+
154
+ fail ImageBombError if width > 5000 || height > 5000
155
+
156
+ # ...
157
+ end
158
+ end
159
+ ```
139
160
 
140
161
  ## Prevent metadata tampering
141
162
 
@@ -153,7 +174,7 @@ app. To guard yourself from such attacks, you can load the
153
174
  cached files on assignment and override the received metadata.
154
175
 
155
176
  ```rb
156
- plugin :restore_cached_data
177
+ Shrine.plugin :restore_cached_data
157
178
  ```
158
179
 
159
180
  ## Limit number of files
@@ -172,6 +193,7 @@ class MyUploader < Shrine
172
193
 
173
194
  Attacher.validate do
174
195
  validate_min_size 10*1024 # 10 KB
196
+ # ...
175
197
  end
176
198
  end
177
199
  ```
@@ -183,6 +205,4 @@ end
183
205
  * [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/)
184
206
 
185
207
  [image bombs]: https://www.bamsoftware.com/hacks/deflate.html
186
- [fastimage]: https://github.com/sdsykes/fastimage
187
- [file]: http://linux.die.net/man/1/file
188
208
  [rack-attack]: https://github.com/kickstarter/rack-attack
@@ -79,8 +79,8 @@ plugin
79
79
 
80
80
  ```rb
81
81
  class MyUploader < Shrine
82
- plugin :upload_options, store: -> (io, context) do
83
- if context[:version] == :thumb
82
+ plugin :upload_options, store: -> (io, derivative: nil, **) do
83
+ if derivative == :thumb
84
84
  { acl: "public-read" }
85
85
  else
86
86
  { acl: "private" }
@@ -6,9 +6,9 @@ attachments implemented with Shrine in your application.
6
6
  ## Callbacks
7
7
 
8
8
  When you first try to test file attachments, you might experience that files
9
- are simply not being promoted (uploaded from temporary to permanent storage).
10
- This is because your tests are likely setup to be wrapped inside database
11
- transactions, and that doesn't work with Shrine callbacks.
9
+ are not being promoted to permanent storage. This is because your tests are
10
+ likely setup to be wrapped inside database transactions, and that doesn't work
11
+ with Shrine callbacks.
12
12
 
13
13
  Specifically, Shrine uses "after commit" callbacks for promoting and deleting
14
14
  attached files. This means that if your tests are wrapped inside transactions,
@@ -18,7 +18,7 @@ happens only after the test has already finished.
18
18
  ```rb
19
19
  # Promoting will happen only after the test transaction commits
20
20
  it "can attach images" do
21
- photo = Photo.create(image: image_file)
21
+ photo = Photo.create(image: file)
22
22
  photo.image.storage_key #=> :cache (we expected it to be promoted to permanent storage)
23
23
  end
24
24
  ```
@@ -37,15 +37,10 @@ end
37
37
  ## Storage
38
38
 
39
39
  If you're using FileSystem storage and your tests run in a single process,
40
- you can switch to [memory storage][shrine-memory], which is both faster and
41
- doesn't require you to clean up anything between tests.
40
+ you can switch to `Shrine::Storage::Memory`, which is both faster and doesn't
41
+ require you to clean up anything between tests.
42
42
 
43
43
  ```rb
44
- # Gemfile
45
- gem "shrine-memory"
46
- ```
47
- ```rb
48
- # test/test_helper.rb
49
44
  require "shrine/storage/memory"
50
45
 
51
46
  Shrine.storages = {
@@ -100,41 +95,88 @@ subdomains when generating URLs.
100
95
 
101
96
  ## Test data
102
97
 
103
- If you're creating test data dynamically using libraries like [factory_bot],
104
- you can have the test file assigned dynamically when the record is created:
98
+ We want to keep our tests fast, so when we're setting up files for tests, we
99
+ want to avoid expensive operations such as file processing and metadata
100
+ extraction.
101
+
102
+ We can start by creating a method which would generate fake attachment data:
105
103
 
106
104
  ```rb
107
- factory :photo do
108
- image { File.open("test/files/image.jpg") }
109
- end
110
- ```
105
+ module TestData
106
+ module_function
111
107
 
112
- On the other hand, if you're setting up test data using Rails' YAML fixtures,
113
- you unfortunately won't be able to use them for assigning files. This is
114
- because Rails fixtures only allow assigning primitive data types, and don't
115
- allow you to specify Shrine attributes - you can only assign to columns
116
- directly.
108
+ def image_data
109
+ attacher = Shrine::Attacher.new
110
+ attacher.set(uploaded_image)
117
111
 
118
- ## Background jobs
112
+ # if you're processing derivatives
113
+ attacher.set_derivatives(
114
+ large: uploaded_image,
115
+ medium: uploaded_image,
116
+ small: uploaded_image,
117
+ )
119
118
 
120
- If you're using background jobs with Shrine, you probably want to make them
121
- synchronous in tests. Your favourite backgrounding library should already
122
- support this, examples:
119
+ attacher.column_data
120
+ end
123
121
 
122
+ def uploaded_image
123
+ file = File.open("test/files/image.jpg", binmode: true)
124
+
125
+ # for performance we skip metadata extraction and assign test metadata
126
+ uploaded_file = Shrine.upload(file, :store, metadata: false)
127
+ uploaded_file.metadata.merge!(
128
+ "size" => file.size,
129
+ "mime_type" => "image/jpeg",
130
+ "filename" => "test.jpg",
131
+ )
132
+
133
+ uploaded_file
134
+ end
135
+ end
136
+ ```
124
137
  ```rb
125
- # Sidekiq
126
- require "sidekiq/testing"
127
- Sidekiq::Testing.inline!
138
+ TestData.image_data #=> '{"id":"...","storage":"...","metadata":{...},"derivatives":{...}}'
128
139
  ```
129
140
 
141
+ With [factory_bot] you can then assign the test attachment data like this:
142
+
130
143
  ```rb
131
- # SuckerPunch
132
- require "sucker_punch/testing/inline"
144
+ factory :photo do
145
+ image_data { TestData.image_data }
146
+ end
133
147
  ```
134
148
 
149
+ With [Rails' YAML fixtures][fixtures] it would look like this:
150
+
151
+ ```erb
152
+ photo:
153
+ image_data: <%= TestData.image_data %>
154
+ ```
155
+
156
+ ## Unit tests
157
+
158
+ For testing attachment in your unit tests, you can assign plain `File` objects:
159
+
135
160
  ```rb
136
- # ActiveJob
137
- ActiveJob::Base.queue_adapter = :inline
161
+ RSpec.describe ImageUploader do
162
+ let(:image) { photo.image }
163
+ let(:derivatives) { photo.image_derivatives }
164
+ let(:photo) { Photo.create(image: File.open("test/files/image.png", "rb")) }
165
+
166
+ it "extracts metadata" do
167
+ expect(image.mime_type).to eq("image/png")
168
+ expect(image.extension).to eq("png")
169
+ expect(image.size).to be_instance_of(Integer)
170
+ expect(image.width).to be_instance_of(Integer)
171
+ expect(image.height).to be_instance_of(Integer)
172
+ end
173
+
174
+ it "generates derivatives" do
175
+ expect(derivatives[:small]).to be_kind_of(Shrine::UploadedFile)
176
+ expect(derivatives[:medium]).to be_kind_of(Shrine::UploadedFile)
177
+ expect(derivatives[:large]).to be_kind_of(Shrine::UploadedFile)
178
+ end
179
+ end
138
180
  ```
139
181
 
140
182
  ## Acceptance tests
@@ -170,119 +212,61 @@ With [Rack::TestApp] you can create multipart file upload requests by using the
170
212
  http.post "/photos", multipart: {"photo[image]" => File.open("test/files/image.jpg")}
171
213
  ```
172
214
 
173
- ## Attachment
174
-
175
- Even though all the file attachment logic is usually encapsulated in your
176
- uploader classes, in general it's still best to test this logic through models.
215
+ ## Background jobs
177
216
 
178
- In your controller the attachment attribute using the uploaded file from the
179
- controller, in Rails case it's an `ActionDispatch::Http::UploadedFile`.
180
- However, you can also assign plain `File` objects, or any other kind of IO-like
181
- objects.
217
+ If you're using background jobs with Shrine, you probably want to make them
218
+ synchronous in tests. See your backgrounding library docs for how to make jobs
219
+ synchronous.
182
220
 
183
221
  ```rb
184
- describe ImageUploader do
185
- it "generates image thumbnails" do
186
- photo = Photo.create(image: File.open("test/files/image.png"))
187
- assert_equal [:small, :medium, :large], photo.image.keys
188
- end
189
- end
222
+ # ActiveJob
223
+ ActiveJob::Base.queue_adapter = :inline
190
224
  ```
191
-
192
- If you want test with an IO object that closely resembles the kind of IO that
193
- is assigned by your web framework, you can use this:
194
-
195
225
  ```rb
196
- require "forwardable"
197
- require "stringio"
198
-
199
- class FakeIO
200
- attr_reader :original_filename, :content_type
201
-
202
- def initialize(content, filename: nil, content_type: nil)
203
- @io = StringIO.new(content)
204
- @original_filename = filename
205
- @content_type = content_type
206
- end
207
-
208
- extend Forwardable
209
- delegate %i[read rewind eof? close size] => :@io
210
- end
226
+ # Sidekiq
227
+ require "sidekiq/testing"
228
+ Sidekiq::Testing.inline!
211
229
  ```
212
-
213
230
  ```rb
214
- describe ImageUploader do
215
- it "generates image thumbnails" do
216
- photo = Photo.create(image: FakeIO.new(File.read("test/files/image.png")))
217
- assert_equal [:small, :medium, :large], photo.image.keys
218
- end
219
- end
231
+ # SuckerPunch
232
+ require "sucker_punch/testing/inline"
220
233
  ```
221
234
 
222
235
  ## Processing
223
236
 
224
- In tests you usually don't want to perform processing, or at least don't want
225
- it to be performed by default (only when you're actually testing it).
226
-
227
- If you're processing only single files, you can override the `Shrine#process`
228
- method in tests to return nil:
237
+ If you're testing your attachment flow which includes processing [derivatives],
238
+ you might want to disable the processing for certain tests. You can do this by
239
+ temporarily overriding the processor:
229
240
 
230
241
  ```rb
231
- class ImageUploader
232
- def process(io, context)
233
- # don't do any processing
234
- end
235
- end
236
- ```
237
-
238
- If you're processing versions, you can override `Shrine#process` to simply
239
- return a hash of unprocessed original files:
240
-
241
- ```rb
242
- class ImageUploader
243
- def process(io, context)
244
- if context[:action] == :store
245
- {small: io.download, medium: io.download, large: io.download}
242
+ module TestMode
243
+ module_function
244
+
245
+ def disable_processing(attacher, processor_name = :default)
246
+ attacher.class.instance_exec do
247
+ original_processor = derivatives_processor
248
+ derivatives_processor(processor_name) { Hash.new }
249
+ yield
250
+ derivatives_processor(processor_name, &original_processor)
246
251
  end
247
252
  end
248
253
  end
249
254
  ```
250
-
251
- However, it's even better to design your processing code in such a way that
252
- it's easier to swap out in tests. In your *application* code you could extract
253
- processing into a single `#call`-able object, and register it inside uploader
254
- generic `opts` hash.
255
-
256
- ```rb
257
- class ImageUploader < Shrine
258
- opts[:processor] = ImageThumbnailsGenerator
259
-
260
- process(:store) do |io, context|
261
- opts[:processor].call(io, context)
262
- end
263
- end
264
- ```
265
-
266
- Now in your tests you can easily swap out `ImageThumbnailsGenerator` with
267
- "fake" processing, which just returns the result in correct format (single file
268
- or hash of versions). Since the only requirement of the processor is that it
269
- responds to `#call`, we can just swap it out for a proc or a lambda:
270
-
271
255
  ```rb
272
- ImageUploader.opts[:processor] = proc do |io, context|
273
- # return unprocessed file(s)
256
+ TestMode.disable_processing(Photo.image_attacher) do
257
+ photo = Photo.new
258
+ photo.file = File.open("test/files/image.png", "rb")
259
+ photo.save
274
260
  end
275
261
  ```
276
262
 
277
- This also has the benefit of allowing you to test `ImageThumbnailsGenerator` in
278
- isolation.
279
-
280
263
  [DatabaseCleaner]: https://github.com/DatabaseCleaner/database_cleaner
281
- [shrine-memory]: https://github.com/shrinerb/shrine-memory
282
264
  [factory_bot]: https://github.com/thoughtbot/factory_bot
265
+ [fixtures]: https://guides.rubyonrails.org/testing.html#the-low-down-on-fixtures
283
266
  [Capybara]: https://github.com/jnicklas/capybara
284
267
  [`#attach_file`]: http://www.rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Actions#attach_file-instance_method
285
268
  [Rack::Test]: https://github.com/brynary/rack-test
286
269
  [Rack::TestApp]: https://github.com/kwatch/rack-test_app
287
270
  [aws-sdk-ruby stubs]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ClientStubs.html
288
271
  [MinIO]: https://min.io/
272
+ [derivatives]: /doc/plugins/derivatives.md#readme