metainspector 5.18.1 → 5.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,206 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'outbound URL policy' do
4
+ let(:url) { 'https://public.example/page' }
5
+ let(:image_url) { 'https://public.example/image.png' }
6
+ let(:html) { '<img src="/image.png">' }
7
+ let(:png) { "\x89PNG\r\n\x1a\n".b + [13].pack('N') + 'IHDR' + [37, 29].pack('NN') + "\x08\x02\x00\x00\x00".b }
8
+
9
+ before do
10
+ stub_request(:get, url).to_return(body: html, headers: { 'Content-Type' => 'text/html' })
11
+ stub_request(:get, image_url).to_return(body: png, headers: { 'Content-Type' => 'image/png; charset=UTF-8' })
12
+ end
13
+
14
+ [false, nil].each do |result|
15
+ it "blocks the initial GET when the filter returns #{result.inspect}" do
16
+ expect { MetaInspector.new(url, url_filter: ->(_) { result }) }.to raise_error(MetaInspector::RequestError)
17
+ expect(a_request(:get, url)).not_to have_been_made
18
+ end
19
+ end
20
+
21
+ it 'passes a standard Ruby URI and permits truthy results' do
22
+ seen = []
23
+ page = MetaInspector.new(url, url_filter: ->(uri) { seen << uri; :allowed })
24
+ expect(page.images.with_size).to eq([[image_url, 37, 29]])
25
+ expect(seen.map(&:to_s)).to eq([url, image_url])
26
+ expect(seen).to all(be_a(URI::HTTP))
27
+ end
28
+
29
+ it 'does not permit mutations of the filter URI to change the request host' do
30
+ filter = ->(uri) { uri.host.replace('internal.example'); true }
31
+ expect(MetaInspector.new(url, url_filter: filter).url).to eq(url)
32
+ expect(a_request(:get, 'https://internal.example/page')).not_to have_been_made
33
+ end
34
+
35
+ [RuntimeError, Faraday::TimeoutError, Faraday::ConnectionFailed, URI::InvalidURIError, Timeout::Error].each do |error_class|
36
+ [:page, :image].each do |target|
37
+ it "preserves a filter's #{error_class} on a #{target}" do
38
+ error = error_class.new('policy failed')
39
+ filter = ->(_) { raise error }
40
+ expect do
41
+ if target == :page
42
+ MetaInspector.new(url, url_filter: filter)
43
+ else
44
+ MetaInspector.new(url, document: html, url_filter: filter).images.with_size
45
+ end
46
+ end.to raise_error { |actual| expect(actual).to equal(error) }
47
+ end
48
+ end
49
+ end
50
+
51
+ it 'checks HEAD and checks GET again after HEAD' do
52
+ stub_request(:head, url).to_return(headers: { 'Content-Type' => 'text/html' })
53
+ calls = 0
54
+ expect do
55
+ MetaInspector.new(url, head_first: true, url_filter: ->(_) { calls += 1; calls == 1 })
56
+ end.to raise_error(MetaInspector::RequestError)
57
+ expect(a_request(:head, url)).to have_been_made.once
58
+ expect(a_request(:get, url)).not_to have_been_made
59
+ end
60
+
61
+ it 'blocks HEAD before making a request' do
62
+ expect { MetaInspector.new(url, head_first: true, url_filter: ->(_) { false }) }.to raise_error(MetaInspector::RequestError)
63
+ expect(a_request(:head, url)).not_to have_been_made
64
+ end
65
+
66
+ [:head, :get].each do |method|
67
+ it "checks every #{method} redirect after the existing redirects-only callback" do
68
+ urls = [url, 'https://public.example/next', 'https://internal.example/private']
69
+ urls.each_cons(2) { |from, to| stub_request(method, from).to_return(status: 302, headers: { 'Location' => to }) }
70
+ seen = []
71
+ redirects = []
72
+ callback = ->(previous, following) { redirects << [previous.url.to_s, following.url.to_s] }
73
+ filter = ->(uri) { seen << uri.to_s; uri.host == 'public.example' }
74
+ expect do
75
+ MetaInspector.new(url, head_first: method == :head, url_filter: filter,
76
+ faraday_options: { redirect: { callback: callback }.freeze }.freeze)
77
+ end.to raise_error(MetaInspector::RequestError)
78
+ expect(seen).to eq(urls)
79
+ expect(redirects).to eq(urls.each_cons(2).to_a)
80
+ expect(a_request(method, urls.last)).not_to have_been_made
81
+ end
82
+ end
83
+
84
+ it 'validates redirect destinations changed by the existing callback' do
85
+ stub_request(:get, url).to_return(status: 302, headers: { 'Location' => '/next' })
86
+ callback = ->(_, following) { following.url.host = 'internal.example' }
87
+ expect do
88
+ MetaInspector.new(url, url_filter: ->(uri) { uri.host == 'public.example' }, faraday_options: { redirect: { callback: callback } })
89
+ end.to raise_error(MetaInspector::RequestError)
90
+ expect(a_request(:get, 'https://internal.example/next')).not_to have_been_made
91
+ end
92
+
93
+ it 'runs policy before a cached response can be reused' do
94
+ cache = Class.new(Hash) do
95
+ def read(key); self[key]; end
96
+ def write(key, value); self[key] = value; end
97
+ end.new
98
+ stub_request(:get, url).to_return(body: html, headers: { 'Cache-Control' => 'public, max-age=3600' })
99
+ permitted = true
100
+ options = { url_filter: ->(_) { permitted }, faraday_options: { faraday_http_cache: { store: cache } } }
101
+ MetaInspector.new(url, options)
102
+ expect(cache).not_to be_empty
103
+ permitted = false
104
+ expect { MetaInspector.new(url, options) }.to raise_error(MetaInspector::RequestError)
105
+ expect(a_request(:get, url)).to have_been_made.once
106
+ end
107
+
108
+ it 'blocks images from inline HTML before downloading' do
109
+ page = MetaInspector.new(url, document: html, url_filter: ->(_) { false })
110
+ expect { page.images.with_size }.to raise_error(MetaInspector::RequestError)
111
+ expect(a_request(:get, url)).not_to have_been_made
112
+ expect(a_request(:get, image_url)).not_to have_been_made
113
+ end
114
+
115
+ it 'checks every image redirect' do
116
+ stub_request(:get, image_url).to_return(status: 302, headers: { 'Location' => '/second.png' })
117
+ stub_request(:get, 'https://public.example/second.png').to_return(status: 302, headers: { 'Location' => 'http://internal.example/private' })
118
+ page = MetaInspector.new(url, document: html, url_filter: ->(uri) { uri.host == 'public.example' })
119
+ expect { page.images.with_size }.to raise_error(MetaInspector::RequestError)
120
+ expect(a_request(:get, 'http://internal.example/private')).not_to have_been_made
121
+ end
122
+
123
+ ['file:///etc/passwd', 'ftp://files.example/image', 'data:image/png;base64,AAAA', 'ftp://files.example/http://public.example/'].each do |other|
124
+ it "rejects non-HTTP network URL #{other}" do
125
+ expect { MetaInspector.new(other) }.to raise_error(MetaInspector::RequestError)
126
+ page = MetaInspector.new(url, document: %(<img src="#{other}">))
127
+ expect { page.images.with_size }.to raise_error(MetaInspector::RequestError)
128
+ end
129
+ end
130
+
131
+ it 'rejects non-HTTP redirect schemes' do
132
+ stub_request(:get, url).to_return(status: 302, headers: { 'Location' => 'file:///etc/passwd' })
133
+ expect { MetaInspector.new(url) }.to raise_error(MetaInspector::RequestError)
134
+ end
135
+
136
+ it 'uses the page headers and redirects-only callback for images' do
137
+ headers = { 'Authorization' => 'Bearer test', 'User-Agent' => 'Policy app' }.freeze
138
+ stub_request(:get, image_url).with(headers: headers).to_return(status: 302, headers: { 'Location' => '/final.png' })
139
+ stub_request(:get, 'https://public.example/final.png').with(headers: headers).to_return(body: png)
140
+ redirects = []
141
+ page = MetaInspector.new(url, document: html, headers: headers,
142
+ faraday_options: { redirect: { callback: ->(old, following) { redirects << [old.url.to_s, following.url.to_s] } } })
143
+ expect(page.images.with_size).to eq([[image_url, 37, 29]])
144
+ expect(redirects).to eq([[image_url, 'https://public.example/final.png']])
145
+ end
146
+
147
+ it 'does not follow image redirects when disabled' do
148
+ stub_request(:get, image_url).to_return(status: 302, headers: { 'Location' => '/final.png' })
149
+ page = MetaInspector.new(url, document: html, allow_redirections: false)
150
+ expect(page.images.with_size).to eq([[image_url, 0, 0]])
151
+ expect(a_request(:get, 'https://public.example/final.png')).not_to have_been_made
152
+ end
153
+
154
+ it 'uses binary IO rather than a URL for image parsing' do
155
+ expect(FastImage).to receive(:size).with(an_instance_of(StringIO)).and_call_original
156
+ expect(MetaInspector.new(url, document: html).images.with_size).to eq([[image_url, 37, 29]])
157
+ end
158
+
159
+ [:to_timeout, :connection_failure, :unparseable].each do |failure|
160
+ it "returns zero dimensions for image #{failure}" do
161
+ request = stub_request(:get, image_url)
162
+ case failure
163
+ when :to_timeout then request.to_timeout
164
+ when :connection_failure then request.to_raise(Faraday::ConnectionFailed)
165
+ when :unparseable then request.to_return(body: 'not an image')
166
+ end
167
+ expect(MetaInspector.new(url, document: html).images.with_size).to eq([[image_url, 0, 0]])
168
+ expect(request).to have_been_requested.once
169
+ end
170
+ end
171
+ it 'rejects a non-callable filter even for inline HTML' do
172
+ expect { MetaInspector.new(url, document: html, url_filter: false) }.to raise_error(ArgumentError, /url_filter/)
173
+ end
174
+
175
+ [
176
+ 'https://other.example/image.png',
177
+ 'http://public.example/image.png',
178
+ 'https://public.example:8443/image.png'
179
+ ].each do |external|
180
+ it "does not send custom page headers to a different image origin #{external}" do
181
+ secret_headers = { 'Authorization' => 'Bearer secret', 'Cookie' => 'session=secret', 'X-Api-Key' => 'secret' }
182
+ stub_request(:get, external).with do |request|
183
+ expect(request.headers.keys).not_to include('Authorization', 'Cookie', 'X-Api-Key')
184
+ true
185
+ end.to_return(body: png)
186
+ page = MetaInspector.new(url, document: %(<img src="#{external}">),
187
+ headers: secret_headers.merge('User-Agent' => 'Policy app'),
188
+ faraday_options: { headers: { 'X-Faraday-Secret' => 'secret' } })
189
+ expect(page.images.with_size).to eq([[external, 37, 29]])
190
+ expect(a_request(:get, external).with { |r| r.headers.key?('X-Faraday-Secret') }).not_to have_been_made
191
+ end
192
+ end
193
+
194
+ it 'retains custom headers on the page origin and strips them on image redirects' do
195
+ secrets = { 'Authorization' => 'Bearer secret', 'Cookie' => 'session=secret', 'X-Api-Key' => 'secret' }
196
+ external = 'https://other.example/image.png'
197
+ stub_request(:get, image_url).with(headers: secrets).to_return(status: 302, headers: { 'Location' => external })
198
+ stub_request(:get, external).with do |request|
199
+ expect(request.headers.keys).not_to include('Authorization', 'Cookie', 'X-Api-Key')
200
+ true
201
+ end.to_return(body: png)
202
+ page = MetaInspector.new(url, document: html, headers: secrets)
203
+ expect(page.images.with_size).to eq([[image_url, 37, 29]])
204
+ end
205
+
206
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metainspector
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.18.1
4
+ version: 5.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaime Iniesta
@@ -326,6 +326,8 @@ files:
326
326
  - lib/meta_inspector.rb
327
327
  - lib/meta_inspector/document.rb
328
328
  - lib/meta_inspector/errors.rb
329
+ - lib/meta_inspector/http_session.rb
330
+ - lib/meta_inspector/image_downloader.rb
329
331
  - lib/meta_inspector/parser.rb
330
332
  - lib/meta_inspector/parsers/base.rb
331
333
  - lib/meta_inspector/parsers/head_links.rb
@@ -335,6 +337,7 @@ files:
335
337
  - lib/meta_inspector/parsers/texts.rb
336
338
  - lib/meta_inspector/request.rb
337
339
  - lib/meta_inspector/url.rb
340
+ - lib/meta_inspector/url_policy.rb
338
341
  - lib/meta_inspector/version.rb
339
342
  - lib/metainspector.rb
340
343
  - meta_inspector.gemspec
@@ -406,6 +409,8 @@ files:
406
409
  - spec/fixtures/wordpress_site.response
407
410
  - spec/fixtures/youtube.response
408
411
  - spec/fixtures/youtube_short_title.response
412
+ - spec/image_download_spec.rb
413
+ - spec/meta_inspector/head_first_spec.rb
409
414
  - spec/meta_inspector/head_links_spec.rb
410
415
  - spec/meta_inspector/images_spec.rb
411
416
  - spec/meta_inspector/links_spec.rb
@@ -416,6 +421,7 @@ files:
416
421
  - spec/parser_spec.rb
417
422
  - spec/request_spec.rb
418
423
  - spec/spec_helper.rb
424
+ - spec/url_filter_spec.rb
419
425
  - spec/url_spec.rb
420
426
  homepage: https://github.com/jaimeiniesta/metainspector
421
427
  licenses: