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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00134b35e4583ae30a00b320468fa362e7290bced2f030a3e02544848f58cde6
4
- data.tar.gz: babed05130568f0a4c306ccaa77da706334bafbd3fe898c961af5e4faaa98c03
3
+ metadata.gz: 01e9dae7534053b14ad6ab963f43aaf4fd6475bae450ddbe78d2dc690ab0d541
4
+ data.tar.gz: ab9674181ce7e7a023b1e58f72c30cb84898358aa09294612b4aeee26cc5057e
5
5
  SHA512:
6
- metadata.gz: 26e8e71a37e1811cfd108c3947d6eaac79576128e2b684e9b1e1d32ee83270f93b4c912b5fa8a324fb643b28dc423ed88ad95bad6efc904fe3af592d9d179844
7
- data.tar.gz: 20ae7f34891ff7dd2284831acad91789656c6817d70577de885744d419ac447d8fac5cb6d7f2e609624835f2b506a85cbd5ba20f6881ae6d402f6b17ae460b82
6
+ metadata.gz: fc6571d503c3c2cbf2e22a6c9af5b75b3479bceaf008e3480e55d7a6ebaf9d5673c3cb61ec64fc29b6081e802afa6b2f5ec321d6247119bda822825a32f4248b
7
+ data.tar.gz: d2188a092148a8ea10865d1cf8e07a8f8a08c6c30760e191efa3a6af3a756821e95e525c116396000c063ba0ec670325900af4b5cb099c222cbfc6d71c5b26b3
@@ -23,7 +23,7 @@ jobs:
23
23
  - uses: actions/checkout@v4
24
24
  - uses: ruby/setup-ruby@v1
25
25
  with:
26
- ruby-version: "3.3"
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", "head"]
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: "3.4" }
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
@@ -4,9 +4,9 @@ gemspec
4
4
 
5
5
  gem "minitest", "~> 5.14"
6
6
  gem "rake", "~> 13.0"
7
- gem "rdoc", "~> 6.3"
7
+ gem "rdoc", "~> 7.2"
8
8
  gem "rubocop", "~> 1.12"
9
9
  unless RUBY_PLATFORM == 'java'
10
10
  gem 'brotli', '>= 0.5'
11
- gem 'zstd-ruby', '~> 1.5'
11
+ gem 'zstd-ruby', '~> 2.0'
12
12
  end
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
@@ -31,7 +31,7 @@ end
31
31
  desc "Run tests"
32
32
  Rake::TestTask.new { |t|
33
33
  t.test_files = Dir['test/**/test*.rb']
34
- t.verbose = true
34
+ # t.verbose = true
35
35
  }
36
36
 
37
37
  task publish_docs: %w[rdoc] do
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.
@@ -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
- def request_add_headers request, headers = {}
589
- @request_headers.each do |k,v|
590
- request[k] = v
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
- case field
595
- when :etag then request["ETag"] = value
596
- when :if_modified_since then request["If-Modified-Since"] = value
597
- when Symbol then
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
- fetch new_url, :get, {}, [],
965
- Mechanize::Page.new, redirects + 1
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
- # Make sure we clear credential headers if being redirected to another site
1069
- if new_uri.host == page.uri.host
1070
- if new_uri.port != page.uri.port
1071
- # https://datatracker.ietf.org/doc/html/rfc6265#section-8.5
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/
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  class Mechanize
3
- VERSION = "2.14.0"
3
+ VERSION = "2.14.1"
4
4
  end
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
@@ -18,13 +18,15 @@
18
18
  </IFRAME>
19
19
  </FRAMESET>
20
20
  <NOFRAMES>
21
- <P>This frameset document contains:
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 test_request_add_headers_request_headers
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.request_add_headers @req
508
+ page = @agent.fetch 'http://example/http_headers'
471
509
 
472
- assert_equal @headers + %w[x-foo], @req.to_hash.keys.sort
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
- assert_includes(headers.keys, "cookie")
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
- assert_match("cookie|name=value", page.body)
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.0
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: 2025-01-05 00:00:00.000000000 Z
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: 3.6.2
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: