image_vise 0.0.26 → 0.0.27

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cb3f1e2fdb272ce247b39af0ac465622ae4aab47
4
- data.tar.gz: af4c923a506fa1b8e960f06cf6ac6686a0cda9d0
3
+ metadata.gz: f5f04e40e483c24a0b40a0b3d2d76ae28be7570e
4
+ data.tar.gz: e79c48aa6995a8325d78a93870b79468e94eb4d5
5
5
  SHA512:
6
- metadata.gz: 436cf6a7fcbafd57c1d531530cf42e08f8aa746fa85aa99dfd3d672e1c6ec9a49c6775387cc7f0fbd183e9abb01c75b698e7a0664b4cd65d7115294bdf334bb5
7
- data.tar.gz: b72ead80e081805e1705ea8f22a69dccca06a70967a5ba731c27938aa3a20469f482c23f62789bce1f9a90d71666d570e304dd2f35e22aee85ba0fea42872a72
6
+ metadata.gz: 6cd787cd19a6eeef389879077ae4ee511f5a8f0bbc388433c4e4cacdb53c8d6320542703bff5d91801b86fa1926b90acbcd95596b2ff12f1fdbc4bbfd32b06ba
7
+ data.tar.gz: 6450c000ad114c2f0e37a1291a174e5cfead4692d8439df7d7c3583261aafbbdac6c9b36897d909a34d0cfbe936edbc5a41c1d89af11ea730f7240bf83c8d015
data/SECURITY.md CHANGED
@@ -8,6 +8,18 @@ URLs are passed as Base64-encoded JSON. The JSON payload is signed using HMAC wi
8
8
  sufficient to prevent too much probing. If this is a critical issue you need to put throttling in front of the application.
9
9
  For checking HMAC values `Rack::Utils.secure_compare` constant-time comparison is used.
10
10
 
11
+ ## Cache bypass protection for randomized query string params
12
+
13
+ ImageVise forbids _any_ query string params except `sig` and `q`. This to prevent the same processing
14
+ URL from being requested repeatedly with the same params but with a different query string param appended,
15
+ like this:
16
+
17
+ * `/thm?q=xYz&sig=abc&exploit=123`
18
+ * `/thm?q=xYz&sig=abc&exploit=234`
19
+
20
+ These URLs would in fact resolve to the same source image and pipeline, but would not be stored in an upstream
21
+ CDN cache because the query string params contain extra data.
22
+
11
23
  ## Protection for remote URLs from HTTP(s) origins
12
24
 
13
25
  Only URLs referring to permitted hosts are going to be permitted for fetching. If there are no host added,
data/image_vise.gemspec CHANGED
@@ -2,11 +2,11 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: image_vise 0.0.26 ruby lib
5
+ # stub: image_vise 0.0.27 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "image_vise"
9
- s.version = "0.0.26"
9
+ s.version = "0.0.27"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib"]
@@ -6,21 +6,18 @@ class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
6
6
 
7
7
  # Initializes a new ParamsChecker from given HTTP server framework
8
8
  # params. The params can be symbol- or string-keyed, does not matter.
9
- def self.to_request(qs_params:, secrets:)
9
+ def self.from_params(qs_params:, secrets:)
10
10
  base64_encoded_params = qs_params.fetch(:q) rescue qs_params.fetch('q')
11
11
  given_signature = qs_params.fetch(:sig) rescue qs_params.fetch('sig')
12
12
 
13
- # Decode Base64 first - this gives us a stable serialized form of the request parameters.
14
- # The encoded parameters might _not_ include ==-padding at the end.
15
- decoded_json = Base64.decode64(base64_encoded_params)
16
-
17
13
  # Check the signature before decoding JSON (since we will be creating symbols)
18
- unless valid_signature?(decoded_json, given_signature, secrets)
14
+ unless valid_signature?(base64_encoded_params, given_signature, secrets)
19
15
  raise SignatureError, "Invalid or missing signature"
20
16
  end
21
17
 
22
18
  # Decode the JSON
23
19
  # (only AFTER the signature has been validated, so we can use symbol keys)
20
+ decoded_json = Base64.decode64(base64_encoded_params)
24
21
  params = JSON.parse(decoded_json, symbolize_names: true)
25
22
 
26
23
  # Pick up the URL and validate it
@@ -35,7 +32,7 @@ class ImageVise::ImageRequest < Ks.strict(:src_url, :pipeline)
35
32
  def to_query_string_params(signed_with_secret)
36
33
  payload = JSON.dump(to_h)
37
34
  base64_enc = Base64.strict_encode64(payload).gsub(/\=+$/, '')
38
- {q: base64_enc, sig: OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, signed_with_secret, payload)}
35
+ {q: base64_enc, sig: OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, signed_with_secret, base64_enc)}
39
36
  end
40
37
 
41
38
  def to_h
@@ -65,10 +65,13 @@ class ImageVise::RenderEngine
65
65
  # and the client already has it. Just respond with a 304.
66
66
  return [304, DEFAULT_HEADERS.dup, []] if env['HTTP_IF_NONE_MATCH']
67
67
 
68
- req = Rack::Request.new(env)
68
+ req = parse_env_into_request(env)
69
69
  bail(405, 'Only GET supported') unless req.get?
70
-
71
- image_request = ImageVise::ImageRequest.to_request(qs_params: req.params, secrets: ImageVise.secret_keys)
70
+
71
+ # Prevent cache bypass DOS attacks by only permitting :sig and :q
72
+ bail(400, 'Too many params') if req.params.length > 2
73
+
74
+ image_request = ImageVise::ImageRequest.from_params(qs_params: req.params, secrets: ImageVise.secret_keys)
72
75
  render_destination_file, render_file_type, etag = process_image_request(image_request)
73
76
  image_rack_response(render_destination_file, render_file_type, etag)
74
77
  rescue *permanent_failures => e
@@ -84,6 +87,16 @@ class ImageVise::RenderEngine
84
87
  raise_exception_or_error_response(e, 500)
85
88
  end
86
89
  end
90
+
91
+ # Parses the Rack environment into a Rack::Reqest. The following methods
92
+ # are going to be called on it: `#get?` and `#params`. You can use this
93
+ # method to override path-to-parameter translation for example.
94
+ #
95
+ # @param rack_env[Hash] the Rack environment
96
+ # @return [#get?, #params] the Rack request or a compatible object
97
+ def parse_env_into_request(rack_env)
98
+ Rack::Request.new(rack_env)
99
+ end
87
100
 
88
101
  # Processes the ImageRequest object created from the request parameters,
89
102
  # and returns a triplet of the File object containing the rendered image,
data/lib/image_vise.rb CHANGED
@@ -8,7 +8,7 @@ require 'base64'
8
8
  require 'rack'
9
9
 
10
10
  class ImageVise
11
- VERSION = '0.0.26'
11
+ VERSION = '0.0.27'
12
12
  S_MUTEX = Mutex.new
13
13
  private_constant :S_MUTEX
14
14
 
@@ -6,25 +6,23 @@ describe ImageVise::ImageRequest do
6
6
  img_params_json = JSON.dump(img_params)
7
7
 
8
8
  q = Base64.encode64(img_params_json)
9
- signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, 'this is a secret', img_params_json)
9
+ signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, 'this is a secret', q)
10
10
  params = {q: q, sig: signature}
11
11
 
12
- image_request = described_class.to_request(qs_params: params, secrets: ['this is a secret'])
12
+ image_request = described_class.from_params(qs_params: params, secrets: ['this is a secret'])
13
13
  request_qs_params = image_request.to_query_string_params('this is a secret')
14
14
  expect(request_qs_params).to be_kind_of(Hash)
15
15
 
16
- image_request_roundtrip = described_class.to_request(qs_params: request_qs_params, secrets: ['this is a secret'])
16
+ image_request_roundtrip = described_class.from_params(qs_params: request_qs_params, secrets: ['this is a secret'])
17
17
  end
18
18
 
19
19
  it 'converts a file:// URL into a URI objectlist' do
20
20
  img_params = {src_url: 'file:///etc/passwd', pipeline: [[:auto_orient, {}]]}
21
21
  img_params_json = JSON.dump(img_params)
22
- signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, 'this is a secret', img_params_json)
23
- params = {
24
- q: Base64.encode64(img_params_json),
25
- sig: signature
26
- }
27
- image_request = described_class.to_request(qs_params: params, secrets: ['this is a secret'])
22
+ q = Base64.encode64(img_params_json)
23
+ signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, 'this is a secret', q)
24
+ params = {q: q, sig: signature}
25
+ image_request = described_class.from_params(qs_params: params, secrets: ['this is a secret'])
28
26
  expect(image_request.src_url).to be_kind_of(URI)
29
27
  end
30
28
 
@@ -39,18 +37,6 @@ describe ImageVise::ImageRequest do
39
37
  end
40
38
  end
41
39
 
42
- describe 'fails with an invalid pipeline' do
43
- it 'when the pipe param is missing'
44
- it 'when the pipe param is empty'
45
- it 'when the pipe param cannot be parsed into a Pipeline'
46
- it 'when the pipe param parses into a Pipeline with zero operators'
47
- end
48
-
49
- describe 'fails with an invalid URL' do
50
- it 'when the URL param is missing'
51
- it 'when the URL param is empty'
52
- end
53
-
54
40
  describe 'fails with an invalid signature' do
55
41
  it 'when the sig param is missing'
56
42
  it 'when the sig param is empty'
@@ -58,14 +44,12 @@ describe ImageVise::ImageRequest do
58
44
  img_params = {src_url: 'http://bucket.s3.aws.com/image.jpg',
59
45
  pipeline: [[:crop, {width: 10, height: 10, gravity: 's'}]]}
60
46
  img_params_json = JSON.dump(img_params)
61
- signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, 'a', img_params_json)
62
- params = {
63
- q: Base64.encode64(img_params_json),
64
- sig: signature
65
- }
47
+ enc = Base64.encode64(img_params_json)
48
+ signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, 'a', enc)
49
+ params = {q: enc, sig: signature}
66
50
 
67
51
  expect {
68
- described_class.to_request(qs_params: params, secrets: ['b'])
52
+ described_class.from_params(qs_params: params, secrets: ['b'])
69
53
  }.to raise_error(/Invalid or missing signature/)
70
54
  end
71
55
  end
@@ -167,6 +167,7 @@ describe ImageVise::RenderEngine do
167
167
  image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
168
168
  params = image_request.to_query_string_params('l33tness')
169
169
 
170
+ expect(app).to receive(:parse_env_into_request).and_call_original
170
171
  expect(app).to receive(:process_image_request).and_call_original
171
172
  expect(app).to receive(:image_rack_response).and_call_original
172
173
  expect(app).to receive(:source_file_type_permitted?).and_call_original
@@ -211,6 +212,19 @@ describe ImageVise::RenderEngine do
211
212
  expect(last_response.headers['Content-Type']).to eq('image/jpeg')
212
213
  end
213
214
 
215
+ it 'forbids a request with an extra GET param' do
216
+ uri = 'file://' + URI.encode(test_image_path)
217
+
218
+ p = ImageVise::Pipeline.new.fit_crop(width: 10, height: 10, gravity: 'c')
219
+ image_request = ImageVise::ImageRequest.new(src_url: uri.to_s, pipeline: p)
220
+ params = image_request.to_query_string_params('l33tness')
221
+
222
+ params[:extra] = '123'
223
+ get '/', params
224
+
225
+ expect(last_response.status).to eq(400)
226
+ end
227
+
214
228
  it 'returns the processed JPEG image as a PNG if it had to get an alpha channel during processing' do
215
229
  uri = Addressable::URI.parse(public_url)
216
230
  ImageVise.add_allowed_host!(uri.host)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: image_vise
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.26
4
+ version: 0.0.27
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov