mechanize 2.14.0 → 2.14.1
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/.github/workflows/upstream.yml +2 -2
- data/CHANGELOG.md +15 -0
- data/Gemfile +2 -2
- data/README.md +7 -0
- data/Rakefile +1 -1
- data/SECURITY.md +11 -0
- data/lib/mechanize/http/agent.rb +83 -29
- data/lib/mechanize/version.rb +1 -1
- data/lib/mechanize.rb +12 -2
- data/test/htdocs/frame_test.html +8 -6
- data/test/htdocs/relative/tc_relative_links.html +1 -1
- data/test/test_mechanize_http_agent.rb +235 -5
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 01e9dae7534053b14ad6ab963f43aaf4fd6475bae450ddbe78d2dc690ab0d541
|
|
4
|
+
data.tar.gz: ab9674181ce7e7a023b1e58f72c30cb84898358aa09294612b4aeee26cc5057e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc6571d503c3c2cbf2e22a6c9af5b75b3479bceaf008e3480e55d7a6ebaf9d5673c3cb61ec64fc29b6081e802afa6b2f5ec321d6247119bda822825a32f4248b
|
|
7
|
+
data.tar.gz: d2188a092148a8ea10865d1cf8e07a8f8a08c6c30760e191efa3a6af3a756821e95e525c116396000c063ba0ec670325900af4b5cb099c222cbfc6d71c5b26b3
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -23,7 +23,7 @@ jobs:
|
|
|
23
23
|
- uses: actions/checkout@v4
|
|
24
24
|
- uses: ruby/setup-ruby@v1
|
|
25
25
|
with:
|
|
26
|
-
ruby-version: "
|
|
26
|
+
ruby-version: "4.0"
|
|
27
27
|
bundler-cache: true
|
|
28
28
|
- run: bundle exec rake rubocop
|
|
29
29
|
|
|
@@ -32,7 +32,7 @@ jobs:
|
|
|
32
32
|
strategy:
|
|
33
33
|
fail-fast: false
|
|
34
34
|
matrix:
|
|
35
|
-
ruby-version: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3", "3.4", "
|
|
35
|
+
ruby-version: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3", "3.4", "4.0"]
|
|
36
36
|
runs-on: ubuntu-latest
|
|
37
37
|
steps:
|
|
38
38
|
- uses: actions/checkout@v4
|
|
@@ -23,7 +23,7 @@ jobs:
|
|
|
23
23
|
strategy:
|
|
24
24
|
fail-fast: false
|
|
25
25
|
matrix:
|
|
26
|
-
ruby-version: ["head"]
|
|
26
|
+
ruby-version: ["head", "truffleruby-head"]
|
|
27
27
|
runs-on: ubuntu-latest
|
|
28
28
|
steps:
|
|
29
29
|
- uses: actions/checkout@v4
|
|
@@ -44,7 +44,7 @@ jobs:
|
|
|
44
44
|
steps:
|
|
45
45
|
- uses: actions/checkout@v4
|
|
46
46
|
- uses: ruby/setup-ruby@v1
|
|
47
|
-
with: { ruby-version: "
|
|
47
|
+
with: { ruby-version: "4.0" }
|
|
48
48
|
- run: |
|
|
49
49
|
bundle add ${{matrix.name}} --git="${{matrix.git}}"
|
|
50
50
|
bundle show
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Mechanize CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 2.14.1 / 2026-08-22
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
* Strip both `Proxy-Authorization` and `Cookie2` headers on a cross-origin redirect. [GHSA-2mwr-xjcg-37j7](https://github.com/sparklemotion/mechanize/security/advisories/GHSA-2mwr-xjcg-37j7)
|
|
8
|
+
* Headers set through `Mechanize#request_headers=` now obey the same cross-origin redirect rules as per-request headers, instead of being re-applied unconditionally to every request. [GHSA-2mwr-xjcg-37j7](https://github.com/sparklemotion/mechanize/security/advisories/GHSA-2mwr-xjcg-37j7)
|
|
9
|
+
* A redirect is now considered cross-origin when the scheme, the host or the port changes, per [RFC 6454](https://datatracker.ietf.org/doc/html/rfc6454). Previously only a host change discarded cookies, and an `https:` to `http:` redirect to the same host carried credentials in cleartext. [GHSA-5jgv-wc2m-xv99](https://github.com/sparklemotion/mechanize/security/advisories/GHSA-5jgv-wc2m-xv99)
|
|
10
|
+
* A `meta` refresh that points cross-origin now discards sensitive request headers, the same as an HTTP redirect. Previously headers set through `Mechanize#request_headers=` followed it unconditionally. [GHSA-c6rp-p8xm-4q9f](https://github.com/sparklemotion/mechanize/security/advisories/GHSA-c6rp-p8xm-4q9f)
|
|
11
|
+
|
|
12
|
+
### Improved
|
|
13
|
+
|
|
14
|
+
* A `meta` refresh now carries the original request's headers, minus any credential withheld for crossing an origin and minus the entity headers of the original request. Previously it carried only the headers set through `Mechanize#request_headers=`.
|
|
15
|
+
* `robots.txt` is fetched without any caller-supplied request headers, on any origin. Previously it received the headers set through `Mechanize#request_headers=`. It is a request mechanize makes on its own behalf.
|
|
16
|
+
|
|
17
|
+
|
|
3
18
|
## 2.14.0 / 2025-01-05
|
|
4
19
|
|
|
5
20
|
* `Mechanize` exposes a `write_timeout` attribute, which is set on the connection if it's supported (e.g., `Net::HTTP::Persistent.write_timeout`). (#586) @maurycy
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -56,6 +56,13 @@ bundle exec rake test
|
|
|
56
56
|
See also Mechanize::TestCase to read about the built-in testing infrastructure.
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
### Security and Vulnerability Reporting
|
|
60
|
+
|
|
61
|
+
Please report vulnerabilities at https://github.com/sparklemotion/mechanize/security
|
|
62
|
+
|
|
63
|
+
See [SECURITY.md](SECURITY.md) for full information and description of our security policy.
|
|
64
|
+
|
|
65
|
+
|
|
59
66
|
## Authors
|
|
60
67
|
|
|
61
68
|
* Eric Hodel
|
data/Rakefile
CHANGED
data/SECURITY.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Security and Vulnerability Reporting
|
|
2
|
+
|
|
3
|
+
The Mechanize Core Contributors take security very seriously and investigate all reported vulnerabilities.
|
|
4
|
+
|
|
5
|
+
If you would like to report a vulnerability or have a security concern regarding Mechanize or how Mechanize is using any of its underlying platform-specific libraries, please [report it via Github](https://github.com/sparklemotion/mechanize/security).
|
|
6
|
+
|
|
7
|
+
Your report should be acknowledged within a week, and you'll receive a more detailed response within two weeks indicating next steps in handling your report.
|
|
8
|
+
|
|
9
|
+
If you have not received a reply to your submission within a week, contact the current security coordinator (Mike Dalessio <mike.dalessio@gmail.com>).
|
|
10
|
+
|
|
11
|
+
If a vulnerability is first reported by you, we will credit you with the discovery in the public disclosure.
|
data/lib/mechanize/http/agent.rb
CHANGED
|
@@ -9,10 +9,14 @@ require 'webrobots'
|
|
|
9
9
|
|
|
10
10
|
class Mechanize::HTTP::Agent
|
|
11
11
|
|
|
12
|
-
CREDENTIAL_HEADERS = ['Authorization']
|
|
13
|
-
COOKIE_HEADERS = ['Cookie']
|
|
12
|
+
CREDENTIAL_HEADERS = ['Authorization', 'Proxy-Authorization']
|
|
13
|
+
COOKIE_HEADERS = ['Cookie', 'Cookie2']
|
|
14
14
|
POST_HEADERS = ['Content-Length', 'Content-MD5', 'Content-Type']
|
|
15
15
|
|
|
16
|
+
# Symbols accepted as header names in the per-request headers hash, and the
|
|
17
|
+
# header each one names.
|
|
18
|
+
SYMBOL_HEADERS = { :etag => 'ETag', :if_modified_since => 'If-Modified-Since' }.freeze
|
|
19
|
+
|
|
16
20
|
# :section: Headers
|
|
17
21
|
|
|
18
22
|
# Disables If-Modified-Since conditional requests (enabled by default)
|
|
@@ -21,7 +25,9 @@ class Mechanize::HTTP::Agent
|
|
|
21
25
|
# Is gzip compression of requests enabled?
|
|
22
26
|
attr_accessor :gzip_enabled
|
|
23
27
|
|
|
24
|
-
# A hash of request headers to be used for every request
|
|
28
|
+
# A hash of request headers to be used for every request. Headers named in
|
|
29
|
+
# CREDENTIAL_HEADERS or COOKIE_HEADERS are withheld from a request that
|
|
30
|
+
# follows a redirect across an origin; see #crosses_origin?.
|
|
25
31
|
attr_accessor :request_headers
|
|
26
32
|
|
|
27
33
|
# The User-Agent header to send
|
|
@@ -240,9 +246,20 @@ class Mechanize::HTTP::Agent
|
|
|
240
246
|
#
|
|
241
247
|
# +redirects+ tracks the number of redirects experienced when retrieving the
|
|
242
248
|
# page. If it is over the redirection_limit an error will be raised.
|
|
249
|
+
#
|
|
250
|
+
# +apply_request_headers+ merges #request_headers into +headers+. It is true
|
|
251
|
+
# for a request the caller initiated and false for one continuing an
|
|
252
|
+
# operation already under way, such as following a redirect, where +headers+
|
|
253
|
+
# already carries the merged set minus anything withheld along the way.
|
|
243
254
|
|
|
244
255
|
def fetch uri, method = :get, headers = {}, params = [],
|
|
245
|
-
referer = current_page, redirects = 0
|
|
256
|
+
referer = current_page, redirects = 0, apply_request_headers = true
|
|
257
|
+
|
|
258
|
+
# Fold the agent defaults into the caller's headers once, at the start of
|
|
259
|
+
# the operation, so that from here on a single hash carries every header
|
|
260
|
+
# and #response_redirect can drop a credential from it for good. Requests
|
|
261
|
+
# continuing an operation pass false and supply the hash they were given.
|
|
262
|
+
headers = merge_request_headers headers if apply_request_headers
|
|
246
263
|
|
|
247
264
|
referer_uri = referer ? referer.uri : nil
|
|
248
265
|
uri = resolve uri, referer
|
|
@@ -313,7 +330,7 @@ class Mechanize::HTTP::Agent
|
|
|
313
330
|
|
|
314
331
|
response_cookies response, uri, page
|
|
315
332
|
|
|
316
|
-
meta = response_follow_meta_refresh response, uri, page, redirects
|
|
333
|
+
meta = response_follow_meta_refresh response, uri, page, redirects, headers
|
|
317
334
|
return meta if meta
|
|
318
335
|
|
|
319
336
|
if robots && page.is_a?(Mechanize::Page)
|
|
@@ -585,23 +602,58 @@ class Mechanize::HTTP::Agent
|
|
|
585
602
|
end
|
|
586
603
|
end
|
|
587
604
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
605
|
+
# The agent defaults, with +headers+ applied on top. A header in +headers+
|
|
606
|
+
# replaces an agent default naming the same header, whatever the spelling,
|
|
607
|
+
# because header names are case-insensitive and the agent's keys may be
|
|
608
|
+
# symbols.
|
|
609
|
+
def merge_request_headers headers
|
|
610
|
+
merged = {}
|
|
611
|
+
|
|
612
|
+
@request_headers.each { |field, value| merged[field.to_s] = value }
|
|
613
|
+
|
|
614
|
+
headers.each do |field, value|
|
|
615
|
+
name = canonical_header_name field
|
|
616
|
+
merged.delete_if { |existing, _| canonical_header_name(existing) == name }
|
|
617
|
+
merged[field] = value
|
|
591
618
|
end
|
|
592
619
|
|
|
620
|
+
merged
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def canonical_header_name field
|
|
624
|
+
(SYMBOL_HEADERS[field] || field).to_s.downcase
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def request_add_headers request, headers = {}
|
|
593
628
|
headers.each do |field, value|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
raise ArgumentError, "unknown header symbol #{field}"
|
|
629
|
+
if Symbol === field then
|
|
630
|
+
name = SYMBOL_HEADERS[field] or
|
|
631
|
+
raise ArgumentError, "unknown header symbol #{field}"
|
|
632
|
+
request[name] = value
|
|
599
633
|
else
|
|
600
634
|
request[field] = value
|
|
601
635
|
end
|
|
602
636
|
end
|
|
603
637
|
end
|
|
604
638
|
|
|
639
|
+
# Does moving from +from_uri+ to +to_uri+ cross an origin? An origin is the
|
|
640
|
+
# scheme, host and port together; see RFC 6454.
|
|
641
|
+
def crosses_origin? from_uri, to_uri
|
|
642
|
+
to_uri.scheme != from_uri.scheme ||
|
|
643
|
+
to_uri.host != from_uri.host ||
|
|
644
|
+
to_uri.port != from_uri.port
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# Should the header named +name+ be withheld from a request issued after a
|
|
648
|
+
# redirect that crossed an origin?
|
|
649
|
+
def drop_after_redirect? name, crossed_origin
|
|
650
|
+
crossed_origin && match_header?(name, CREDENTIAL_HEADERS + COOKIE_HEADERS)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def match_header? name, candidates
|
|
654
|
+
candidates.any? { |candidate| name.to_s.casecmp?(candidate) }
|
|
655
|
+
end
|
|
656
|
+
|
|
605
657
|
def request_auth request, uri
|
|
606
658
|
base_uri = uri + '/'
|
|
607
659
|
base_uri.user &&= nil
|
|
@@ -874,7 +926,7 @@ class Mechanize::HTTP::Agent
|
|
|
874
926
|
raise Mechanize::UnauthorizedError.new(page, challenges, message)
|
|
875
927
|
end
|
|
876
928
|
|
|
877
|
-
fetch uri, request.method.downcase.to_sym, headers, params, referer
|
|
929
|
+
fetch uri, request.method.downcase.to_sym, headers, params, referer, 0, false
|
|
878
930
|
end
|
|
879
931
|
|
|
880
932
|
def response_content_encoding response, body_io
|
|
@@ -951,7 +1003,7 @@ class Mechanize::HTTP::Agent
|
|
|
951
1003
|
end
|
|
952
1004
|
end
|
|
953
1005
|
|
|
954
|
-
def response_follow_meta_refresh response, uri, page, redirects
|
|
1006
|
+
def response_follow_meta_refresh response, uri, page, redirects, headers = {}
|
|
955
1007
|
delay, new_url = get_meta_refresh(response, uri, page)
|
|
956
1008
|
return nil unless delay
|
|
957
1009
|
new_url = new_url ? secure_resolve!(new_url, page) : uri
|
|
@@ -961,8 +1013,16 @@ class Mechanize::HTTP::Agent
|
|
|
961
1013
|
|
|
962
1014
|
sleep delay
|
|
963
1015
|
@history.push(page, page.uri)
|
|
964
|
-
|
|
965
|
-
|
|
1016
|
+
|
|
1017
|
+
headers = headers.dup
|
|
1018
|
+
|
|
1019
|
+
# The refresh is fetched with GET, so the original request's entity headers
|
|
1020
|
+
# must not describe it.
|
|
1021
|
+
POST_HEADERS.each { |key| headers.delete_if { |h, _| h.to_s.casecmp?(key) } }
|
|
1022
|
+
headers.delete_if { |h, _| drop_after_redirect? h, crosses_origin?(uri, new_url) }
|
|
1023
|
+
|
|
1024
|
+
fetch new_url, :get, headers, [],
|
|
1025
|
+
Mechanize::Page.new, redirects + 1, false
|
|
966
1026
|
end
|
|
967
1027
|
|
|
968
1028
|
def response_log response
|
|
@@ -1065,18 +1125,12 @@ class Mechanize::HTTP::Agent
|
|
|
1065
1125
|
headers.delete_if { |h| h.casecmp?(key) }
|
|
1066
1126
|
end
|
|
1067
1127
|
|
|
1068
|
-
#
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
# cookies are OK to be shared across ports on the same host
|
|
1073
|
-
CREDENTIAL_HEADERS.each { |ch| headers.delete_if { |h| h.casecmp?(ch) } }
|
|
1074
|
-
end
|
|
1075
|
-
else
|
|
1076
|
-
(COOKIE_HEADERS + CREDENTIAL_HEADERS).each { |ch| headers.delete_if { |h| h.casecmp?(ch) } }
|
|
1077
|
-
end
|
|
1128
|
+
# Sensitive headers must not follow a redirect across an origin.
|
|
1129
|
+
crossed_origin = crosses_origin? page.uri, new_uri
|
|
1130
|
+
|
|
1131
|
+
headers.delete_if { |h, _| drop_after_redirect? h, crossed_origin }
|
|
1078
1132
|
|
|
1079
|
-
fetch new_uri, redirect_method, headers, [], referer, redirects + 1
|
|
1133
|
+
fetch new_uri, redirect_method, headers, [], referer, redirects + 1, false
|
|
1080
1134
|
end
|
|
1081
1135
|
|
|
1082
1136
|
# :section: Robots
|
|
@@ -1087,7 +1141,7 @@ class Mechanize::HTTP::Agent
|
|
|
1087
1141
|
robots_mutex.synchronize do
|
|
1088
1142
|
Thread.current[RobotsKey] = true
|
|
1089
1143
|
begin
|
|
1090
|
-
fetch(uri).body
|
|
1144
|
+
fetch(uri, :get, {}, [], current_page, 0, false).body
|
|
1091
1145
|
rescue Mechanize::ResponseCodeError => e
|
|
1092
1146
|
case e.response_code
|
|
1093
1147
|
when /\A4\d\d\z/
|
data/lib/mechanize/version.rb
CHANGED
data/lib/mechanize.rb
CHANGED
|
@@ -1011,7 +1011,16 @@ Use of #auth and #basic_auth are deprecated due to a security vulnerability.
|
|
|
1011
1011
|
end
|
|
1012
1012
|
|
|
1013
1013
|
##
|
|
1014
|
-
# A hash of custom request headers that will be sent on every request
|
|
1014
|
+
# A hash of custom request headers that will be sent on every request.
|
|
1015
|
+
#
|
|
1016
|
+
# Headers named in Mechanize::HTTP::Agent::CREDENTIAL_HEADERS or
|
|
1017
|
+
# COOKIE_HEADERS are withheld from a request that follows a redirect to
|
|
1018
|
+
# another origin, where an origin is the scheme, host and port together, and
|
|
1019
|
+
# stay withheld for the rest of that operation. A meta refresh to another
|
|
1020
|
+
# origin is treated the same way.
|
|
1021
|
+
#
|
|
1022
|
+
# The robots.txt lookup carries none of these headers, on any origin. It is
|
|
1023
|
+
# a request mechanize makes on its own behalf.
|
|
1015
1024
|
|
|
1016
1025
|
def request_headers
|
|
1017
1026
|
@agent.request_headers
|
|
@@ -1019,7 +1028,8 @@ Use of #auth and #basic_auth are deprecated due to a security vulnerability.
|
|
|
1019
1028
|
|
|
1020
1029
|
##
|
|
1021
1030
|
# Replaces the custom request headers that will be sent on every request
|
|
1022
|
-
# with +request_headers
|
|
1031
|
+
# with +request_headers+. See #request_headers for the credentials
|
|
1032
|
+
# withheld after a redirect to another origin.
|
|
1023
1033
|
|
|
1024
1034
|
def request_headers= request_headers
|
|
1025
1035
|
@agent.request_headers = request_headers
|
data/test/htdocs/frame_test.html
CHANGED
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
</IFRAME>
|
|
19
19
|
</FRAMESET>
|
|
20
20
|
<NOFRAMES>
|
|
21
|
-
|
|
22
|
-
<UL>
|
|
23
|
-
<LI><A href="/google.html">Some neat contents</A>
|
|
24
|
-
<LI><A href="/form_test.html" class="bar">Form Test</A>
|
|
25
|
-
<LI><A href="/file_upload.html">Some other neat contents</A>
|
|
26
|
-
</UL>
|
|
21
|
+
Your user agent does not support frames.
|
|
27
22
|
</NOFRAMES>
|
|
23
|
+
<P>This frameset document contains:
|
|
24
|
+
<UL>
|
|
25
|
+
<LI><A href="/google.html">Some neat contents</A>
|
|
26
|
+
<LI><A href="/form_test.html" class="bar">Form Test</A>
|
|
27
|
+
<LI><A href="/file_upload.html">Some other neat contents</A>
|
|
28
|
+
</UL>
|
|
29
|
+
</P>
|
|
28
30
|
</FRAMESET>
|
|
29
31
|
</HTML>
|
|
30
32
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<body>
|
|
3
3
|
<a href="../tc_relative_links.html">dot dot slash</a>
|
|
4
4
|
<a href="../../../../../tc_relative_links.html">too many dots</a>
|
|
5
|
+
<a href="?a=b">just the query string</A>
|
|
5
6
|
<FRAMESET cols="20%, 80%">
|
|
6
7
|
<FRAMESET rows="100, 200, 200">
|
|
7
8
|
<FRAME name="frame1" src="../tc_relative_links.html">
|
|
@@ -13,7 +14,6 @@
|
|
|
13
14
|
[Your user agent does not support frames or is currently configured
|
|
14
15
|
not to display frames. However, you may visit
|
|
15
16
|
<A href="foo.html">the related document.</A>]
|
|
16
|
-
<a href="?a=b">just the query string</A>
|
|
17
17
|
</IFRAME>
|
|
18
18
|
</FRAMESET>
|
|
19
19
|
</FRAMESET>
|
|
@@ -464,12 +464,50 @@ class TestMechanizeHttpAgent < Mechanize::TestCase
|
|
|
464
464
|
assert_equal @headers, @req.to_hash.keys.sort
|
|
465
465
|
end
|
|
466
466
|
|
|
467
|
-
def
|
|
467
|
+
def test_fetch_accepts_symbol_names_in_request_headers
|
|
468
|
+
@agent.request_headers = { Authorization: 'Bearer tokensecret' }
|
|
469
|
+
|
|
470
|
+
page = @agent.fetch 'http://example/http_headers'
|
|
471
|
+
|
|
472
|
+
assert_match 'authorization|Bearer tokensecret', page.body
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def test_fetch_lets_a_request_header_override_any_spelling_of_an_agent_header
|
|
476
|
+
@agent.request_headers = { 'Authorization' => 'agent-one', 'authorization' => 'agent-two' }
|
|
477
|
+
|
|
478
|
+
page = @agent.fetch 'http://example/http_headers', :get,
|
|
479
|
+
{ 'Authorization' => 'per-request' }
|
|
480
|
+
|
|
481
|
+
assert_match 'authorization|per-request', page.body
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def test_meta_refresh_drops_entity_headers
|
|
485
|
+
@agent.follow_meta_refresh = true
|
|
486
|
+
|
|
487
|
+
uri = URI 'http://example/session'
|
|
488
|
+
response = Net::HTTPOK.new '1.1', '200', 'OK'
|
|
489
|
+
response['Refresh'] = '0;url=/http_headers'
|
|
490
|
+
page = Mechanize::Page.new uri, response, '', 200, @mech
|
|
491
|
+
|
|
492
|
+
@agent.response_follow_meta_refresh(response, uri, page, 0,
|
|
493
|
+
{ 'Content-Length' => '7',
|
|
494
|
+
'Content-Type' => 'text/plain',
|
|
495
|
+
'Content-MD5' => 'deadbeef' })
|
|
496
|
+
|
|
497
|
+
request = requests.last
|
|
498
|
+
|
|
499
|
+
assert_equal 'GET', request.method
|
|
500
|
+
assert_nil request['Content-Length']
|
|
501
|
+
assert_nil request['Content-Type']
|
|
502
|
+
assert_nil request['Content-MD5']
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def test_fetch_applies_request_headers
|
|
468
506
|
@agent.request_headers['X-Foo'] = 'bar'
|
|
469
507
|
|
|
470
|
-
@agent.
|
|
508
|
+
page = @agent.fetch 'http://example/http_headers'
|
|
471
509
|
|
|
472
|
-
|
|
510
|
+
assert_match 'x-foo|bar', page.body
|
|
473
511
|
end
|
|
474
512
|
|
|
475
513
|
def test_request_add_headers_symbol
|
|
@@ -1300,6 +1338,33 @@ class TestMechanizeHttpAgent < Mechanize::TestCase
|
|
|
1300
1338
|
assert_equal uri, page.uri
|
|
1301
1339
|
end
|
|
1302
1340
|
|
|
1341
|
+
def test_response_follow_meta_refresh_to_cross_origin_drops_agent_request_headers
|
|
1342
|
+
uri = URI.parse 'http://example/'
|
|
1343
|
+
|
|
1344
|
+
body = <<-BODY
|
|
1345
|
+
<title></title>
|
|
1346
|
+
<meta http-equiv="refresh" content="0;url=http://trap/http_headers">
|
|
1347
|
+
BODY
|
|
1348
|
+
|
|
1349
|
+
page = Mechanize::Page.new(uri, nil, body, 200, @mech)
|
|
1350
|
+
|
|
1351
|
+
@agent.follow_meta_refresh = true
|
|
1352
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1353
|
+
|
|
1354
|
+
page = @agent.response_follow_meta_refresh @res, uri, page, 0
|
|
1355
|
+
|
|
1356
|
+
refute_match("authorization|Bearer tokensecret", page.body)
|
|
1357
|
+
end
|
|
1358
|
+
|
|
1359
|
+
def test_meta_refresh_to_same_origin_keeps_agent_request_headers
|
|
1360
|
+
@agent.follow_meta_refresh = true
|
|
1361
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1362
|
+
|
|
1363
|
+
page = @agent.fetch 'http://example/http_refresh?refresh_url=/http_headers'
|
|
1364
|
+
|
|
1365
|
+
assert_match("authorization|Bearer tokensecret", page.body)
|
|
1366
|
+
end
|
|
1367
|
+
|
|
1303
1368
|
def test_response_follow_meta_refresh_limit
|
|
1304
1369
|
uri = URI.parse 'http://example/#id+1'
|
|
1305
1370
|
|
|
@@ -1669,6 +1734,171 @@ class TestMechanizeHttpAgent < Mechanize::TestCase
|
|
|
1669
1734
|
refute_match("cookie|name=value", page.body)
|
|
1670
1735
|
end
|
|
1671
1736
|
|
|
1737
|
+
def test_response_redirect_to_cross_site_strips_all_credential_headers
|
|
1738
|
+
@agent.redirect_ok = true
|
|
1739
|
+
|
|
1740
|
+
headers = {
|
|
1741
|
+
'Range' => 'bytes=0-9999',
|
|
1742
|
+
'Proxy-Authorization' => 'Basic proxysecret',
|
|
1743
|
+
'Cookie2' => 'version=1',
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
page = html_page ''
|
|
1747
|
+
page = @agent.response_redirect({ 'Location' => 'http://trap/http_headers' }, :get,
|
|
1748
|
+
page, 0, headers)
|
|
1749
|
+
|
|
1750
|
+
refute_includes(headers.keys, "Proxy-Authorization")
|
|
1751
|
+
refute_includes(headers.keys, "Cookie2")
|
|
1752
|
+
|
|
1753
|
+
refute_match("proxy-authorization|Basic proxysecret", page.body)
|
|
1754
|
+
refute_match("cookie2|version=1", page.body)
|
|
1755
|
+
end
|
|
1756
|
+
|
|
1757
|
+
def test_response_redirect_to_same_site_keeps_all_credential_headers
|
|
1758
|
+
@agent.redirect_ok = true
|
|
1759
|
+
|
|
1760
|
+
headers = {
|
|
1761
|
+
'Proxy-Authorization' => 'Basic proxysecret',
|
|
1762
|
+
'Cookie2' => 'version=1',
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
page = html_page ''
|
|
1766
|
+
page = @agent.response_redirect({ 'Location' => '/http_headers' }, :get,
|
|
1767
|
+
page, 0, headers)
|
|
1768
|
+
|
|
1769
|
+
assert_match("proxy-authorization|Basic proxysecret", page.body)
|
|
1770
|
+
assert_match("cookie2|version=1", page.body)
|
|
1771
|
+
end
|
|
1772
|
+
|
|
1773
|
+
def test_response_redirect_to_cross_site_drops_agent_request_headers
|
|
1774
|
+
@agent.redirect_ok = true
|
|
1775
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret', 'Cookie' => 'name=value' }
|
|
1776
|
+
|
|
1777
|
+
page = html_page ''
|
|
1778
|
+
page = @agent.response_redirect({ 'Location' => 'http://trap/http_headers' }, :get,
|
|
1779
|
+
page, 0, {})
|
|
1780
|
+
|
|
1781
|
+
refute_match("authorization|Bearer tokensecret", page.body)
|
|
1782
|
+
refute_match("cookie|name=value", page.body)
|
|
1783
|
+
end
|
|
1784
|
+
|
|
1785
|
+
def test_redirect_to_cross_site_keeps_insensitive_agent_request_headers
|
|
1786
|
+
@agent.redirect_ok = true
|
|
1787
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret', 'X-Api-Key' => 'apikey' }
|
|
1788
|
+
|
|
1789
|
+
page = @agent.fetch 'http://example/redirect', :get,
|
|
1790
|
+
{ 'X-Location' => 'http://trap/http_headers' }
|
|
1791
|
+
|
|
1792
|
+
refute_match("authorization|Bearer tokensecret", page.body)
|
|
1793
|
+
assert_match("x-api-key|apikey", page.body)
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1796
|
+
def test_redirect_to_same_site_keeps_agent_request_headers
|
|
1797
|
+
@agent.redirect_ok = true
|
|
1798
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1799
|
+
|
|
1800
|
+
page = @agent.fetch 'http://example/redirect', :get, { 'X-Location' => '/http_headers' }
|
|
1801
|
+
|
|
1802
|
+
assert_match("authorization|Bearer tokensecret", page.body)
|
|
1803
|
+
end
|
|
1804
|
+
|
|
1805
|
+
def test_response_redirect_to_downgraded_scheme_strips_credential
|
|
1806
|
+
@agent.redirect_ok = true
|
|
1807
|
+
|
|
1808
|
+
headers = { 'AUTHORIZATION' => 'Basic xxx', 'cookie' => 'name=value' }
|
|
1809
|
+
|
|
1810
|
+
page = page 'https://example:8443/', 'text/html'
|
|
1811
|
+
page = @agent.response_redirect({ 'Location' => 'http://example:8443/http_headers' }, :get,
|
|
1812
|
+
page, 0, headers)
|
|
1813
|
+
|
|
1814
|
+
refute_includes(headers.keys, "AUTHORIZATION")
|
|
1815
|
+
refute_includes(headers.keys, "cookie")
|
|
1816
|
+
|
|
1817
|
+
refute_match("authorization|Basic xxx", page.body)
|
|
1818
|
+
refute_match("cookie|name=value", page.body)
|
|
1819
|
+
end
|
|
1820
|
+
|
|
1821
|
+
def test_response_redirect_to_downgraded_scheme_drops_agent_request_headers
|
|
1822
|
+
@agent.redirect_ok = true
|
|
1823
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1824
|
+
|
|
1825
|
+
page = page 'https://example:8443/', 'text/html'
|
|
1826
|
+
page = @agent.response_redirect({ 'Location' => 'http://example:8443/http_headers' }, :get,
|
|
1827
|
+
page, 0, {})
|
|
1828
|
+
|
|
1829
|
+
refute_match("authorization|Bearer tokensecret", page.body)
|
|
1830
|
+
end
|
|
1831
|
+
|
|
1832
|
+
def test_response_redirect_to_different_port_strips_cookie
|
|
1833
|
+
@agent.redirect_ok = true
|
|
1834
|
+
|
|
1835
|
+
headers = { 'cookie' => 'name=value' }
|
|
1836
|
+
|
|
1837
|
+
page = html_page ''
|
|
1838
|
+
page = @agent.response_redirect({ 'Location' => 'http://example:81/http_headers' }, :get,
|
|
1839
|
+
page, 0, headers)
|
|
1840
|
+
|
|
1841
|
+
refute_match("cookie|name=value", page.body)
|
|
1842
|
+
end
|
|
1843
|
+
|
|
1844
|
+
# Every request the agent made to a host other than the one the credential
|
|
1845
|
+
# was set for.
|
|
1846
|
+
def foreign_requests
|
|
1847
|
+
requests.reject { |request| request['host'] == 'example' }
|
|
1848
|
+
end
|
|
1849
|
+
|
|
1850
|
+
def test_second_redirect_hop_on_a_foreign_origin_does_not_restore_credentials
|
|
1851
|
+
@agent.redirect_ok = true
|
|
1852
|
+
@agent.redirection_limit = 2
|
|
1853
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1854
|
+
|
|
1855
|
+
assert_raises Mechanize::RedirectLimitReachedError do
|
|
1856
|
+
@agent.fetch 'http://example/redirect', :get,
|
|
1857
|
+
{ 'X-Location' => 'http://trap/redirect' }
|
|
1858
|
+
end
|
|
1859
|
+
|
|
1860
|
+
assert_operator foreign_requests.length, :>, 1
|
|
1861
|
+
assert_empty foreign_requests.select { |r| r['Authorization'] }
|
|
1862
|
+
end
|
|
1863
|
+
|
|
1864
|
+
def test_authentication_retry_on_a_foreign_origin_does_not_restore_credentials
|
|
1865
|
+
@agent.redirect_ok = true
|
|
1866
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1867
|
+
@agent.add_auth URI('http://trap/'), 'user', 'pass'
|
|
1868
|
+
|
|
1869
|
+
begin
|
|
1870
|
+
@agent.fetch 'http://example/redirect', :get,
|
|
1871
|
+
{ 'X-Location' => 'http://trap/basic_auth' }
|
|
1872
|
+
rescue Mechanize::UnauthorizedError
|
|
1873
|
+
# a leaked bearer token overwrites the Basic credential and the retry fails
|
|
1874
|
+
end
|
|
1875
|
+
|
|
1876
|
+
assert_operator foreign_requests.length, :>, 1
|
|
1877
|
+
assert_empty foreign_requests.select { |r| r['Authorization'] == 'Bearer tokensecret' }
|
|
1878
|
+
end
|
|
1879
|
+
|
|
1880
|
+
def test_meta_refresh_to_a_second_foreign_origin_does_not_restore_credentials
|
|
1881
|
+
@agent.redirect_ok = true
|
|
1882
|
+
@agent.follow_meta_refresh = true
|
|
1883
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1884
|
+
|
|
1885
|
+
@agent.fetch 'http://example/redirect', :get,
|
|
1886
|
+
{ 'X-Location' => 'http://trap/http_refresh?refresh_url=/http_headers' }
|
|
1887
|
+
|
|
1888
|
+
assert_nil requests.find { |r| r.path == '/http_headers' }['Authorization']
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
def test_robots_lookup_on_a_foreign_origin_does_not_send_credentials
|
|
1892
|
+
@agent.redirect_ok = true
|
|
1893
|
+
@agent.robots = true
|
|
1894
|
+
@agent.request_headers = { 'Authorization' => 'Bearer tokensecret' }
|
|
1895
|
+
|
|
1896
|
+
@agent.fetch 'http://example/redirect', :get,
|
|
1897
|
+
{ 'X-Location' => 'http://trap/http_headers' }
|
|
1898
|
+
|
|
1899
|
+
assert_nil requests.find { |r| r.path == '/robots.txt' }['Authorization']
|
|
1900
|
+
end
|
|
1901
|
+
|
|
1672
1902
|
def test_response_redirect_to_same_site_with_credential
|
|
1673
1903
|
@agent.redirect_ok = true
|
|
1674
1904
|
|
|
@@ -1704,11 +1934,11 @@ class TestMechanizeHttpAgent < Mechanize::TestCase
|
|
|
1704
1934
|
page, 0, headers)
|
|
1705
1935
|
|
|
1706
1936
|
refute_includes(headers.keys, "AUTHORIZATION")
|
|
1707
|
-
|
|
1937
|
+
refute_includes(headers.keys, "cookie")
|
|
1708
1938
|
|
|
1709
1939
|
assert_match("range|bytes=0-9999", page.body)
|
|
1710
1940
|
refute_match("authorization|Basic xxx", page.body)
|
|
1711
|
-
|
|
1941
|
+
refute_match("cookie|name=value", page.body)
|
|
1712
1942
|
end
|
|
1713
1943
|
|
|
1714
1944
|
def test_response_redirect_not_ok
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mechanize
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.14.
|
|
4
|
+
version: 2.14.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Hodel
|
|
@@ -11,7 +11,7 @@ authors:
|
|
|
11
11
|
- Lee Jarvis
|
|
12
12
|
bindir: bin
|
|
13
13
|
cert_chain: []
|
|
14
|
-
date:
|
|
14
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
15
15
|
dependencies:
|
|
16
16
|
- !ruby/object:Gem::Dependency
|
|
17
17
|
name: addressable
|
|
@@ -232,10 +232,11 @@ email:
|
|
|
232
232
|
executables: []
|
|
233
233
|
extensions: []
|
|
234
234
|
extra_rdoc_files:
|
|
235
|
+
- CHANGELOG.md
|
|
235
236
|
- EXAMPLES.rdoc
|
|
236
237
|
- GUIDE.rdoc
|
|
237
|
-
- CHANGELOG.md
|
|
238
238
|
- README.md
|
|
239
|
+
- SECURITY.md
|
|
239
240
|
files:
|
|
240
241
|
- ".autotest"
|
|
241
242
|
- ".github/dependabot.yml"
|
|
@@ -250,6 +251,7 @@ files:
|
|
|
250
251
|
- LICENSE.txt
|
|
251
252
|
- README.md
|
|
252
253
|
- Rakefile
|
|
254
|
+
- SECURITY.md
|
|
253
255
|
- examples/flickr_upload.rb
|
|
254
256
|
- examples/latest_user_agents.rb
|
|
255
257
|
- examples/mech-dump.rb
|
|
@@ -474,7 +476,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
474
476
|
- !ruby/object:Gem::Version
|
|
475
477
|
version: '0'
|
|
476
478
|
requirements: []
|
|
477
|
-
rubygems_version:
|
|
479
|
+
rubygems_version: 4.0.16
|
|
478
480
|
specification_version: 4
|
|
479
481
|
summary: The Mechanize library is used for automating interaction with websites
|
|
480
482
|
test_files:
|