net-ssh 8.0.0.rc1 → 8.0.0.rc3

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: aad28c615382be2934f9a43923ede5e5eed71383f3af54a0440ebf94ad40dc1a
4
- data.tar.gz: 938c219b1073d44e27de4b5d4c95c01df2903d52f6b737454fb504d878ee6592
3
+ metadata.gz: 03f0ed4c48532bcfa337a176d655407155043de284d1428cd51a4bf2b7cb2f58
4
+ data.tar.gz: 49e8bbef56ac3139f5bb05cfad70afb33dda44227e9ce17f4424bd5aed749b95
5
5
  SHA512:
6
- metadata.gz: a9663038da70081bafeaa5a9bfabc0241f7c249386fc6f475f09611d697c6db00161624777ec92abe0ae8f301f4ba20b2ba3ea869766ebccd6c11648acf5834b
7
- data.tar.gz: d8eb01f0e3aad7af012363f211873842c9b80e382d304fb0c3faef045d3eea4ec389ec6aa54be49c600a70bacb566c23b18f0aa0d2d6decf5631169bec01aa59
6
+ metadata.gz: 56ec747abdff1eb6a0cbfb03adb3f880246372afe864f421c4241881671b9ce49dc1dc37a30c9e75e52526284b7c05caab1f14d367f94a1cdd422e1cf4aea93d
7
+ data.tar.gz: df092fc1f9da4c85eaf3b305890e0635b9d16d72b3bf05711180a9c28652d97a987be3011bbf7610363d0a9412dafa056c527172519a92c2bf77e7f228bcade8
checksums.yaml.gz.sig CHANGED
@@ -1 +1 @@
1
- `t�����}k?G�$�ܸ���j�m_bݗ�A�w��Щ}�k�� t�)d�`������V� T��E�#u��҉&ۈ�0~��КKK��J-�֨J�}~s�7b�Z�<�grN�?\jAv�T∖ٙ��Չ�<[*ɇ���Ε4uVGSh����ۣ3��H��ʟ�R��թ.uWu���,�L����˓���* :����fVPy��f�;�C�⥒�)���>���1�BC�g���l^hg{`��m�5%��Yg�'�Ԇ���
1
+ E��x��~o�$�>�d"W���f��WZ4���VE���[��*{dO�8F��dsd���E��I~���]�uԑL���=����= ]չ�Ӈ���I�>���J���Ç���;q=G� �B=�s,��!���9�-M~����������~�J�V:�a�o�s(�7 D6r|z�+H>� ��y�=���QF�)��R�m��)�E݇AO�-o�c��d�q�.��j�0�t���]�r$� ���щ}5�=d����
@@ -1,6 +1,8 @@
1
1
  name: Rubocop
2
2
 
3
- on: [push]
3
+ on:
4
+ pull_request:
5
+ push: { branches: master }
4
6
 
5
7
  jobs:
6
8
  build:
data/CHANGES.txt CHANGED
@@ -1,3 +1,20 @@
1
+ === 8.0.0 rc3
2
+
3
+ * Fix "Calling Net::SSH::Buffer methods on HostKeyEntries PubKey is deprecated" warnings printed on every connection to a host in known_hosts [#1026, #1027]
4
+ * `ProxyJump none` now means no proxy instead of jumping through a host named "none", and `ProxyCommand none` is matched case-insensitively, as in OpenSSH [#893, #972, #1030]
5
+ * Send SIGHUP to the ProxyCommand process on close, as OpenSSH does, so tunnels such as EC2 Instance Connect Endpoint no longer hang at exit [#947]
6
+ * Reconnect when the ssh-agent closes the connection on the legacy version request (e.g. older Proton Pass) instead of raising FrozenError [#1008]
7
+ * ssh_config: parse `Key = value` lines like OpenSSH (the value no longer keeps the leading space, and the keyword ends at the first `=`), and honor ProxyJump/ProxyCommand before the first Host [#1034]
8
+
9
+ === 8.0.0 rc2
10
+
11
+ * Security: refuse agent channels opened by the server unless agent forwarding was requested (`forward_agent`, ForwardAgent, or `Forward#agent`); any use of port forwarding let a malicious server use the local ssh-agent [GHSA-qpg5-fx6p-8cq3]
12
+ * Security: reject host and user names containing shell metacharacters, and non-numeric ports, before substituting them into ProxyCommand/ProxyJump commands, as OpenSSH does since 9.6 [GHSA-3x7f-q7wf-fp73]
13
+ * Security: reject OpenSSH private keys claiming more than 2**20 bcrypt rounds, and only run the KDF once a passphrase is given [GHSA-rpq3-v334-f5pm]
14
+ * Security: bound the server's identification banner like OpenSSH (8192-byte lines, 1024 lines), and apply `:timeout` to the whole version exchange instead of only its first byte [GHSA-r75w-7fhp-84w8]
15
+ * Security: reject packet lengths below 5 or above 256 KB, as OpenSSH does [GHSA-pfqh-p9vh-wx4p]
16
+ * Security: treat unexpected packets during the initial key exchange as a protocol error, and bound packets queued during a re-key [GHSA-ggxh-cfwq-5xqc]
17
+
1
18
  === 8.0.0 rc1
2
19
 
3
20
  * BREAKING: TCP_NODELAY is now set on the connection socket by default, as OpenSSH does; pass `no_delay: false` to keep Nagle's algorithm on [#1017]
@@ -17,6 +17,9 @@ module Net
17
17
  # An exception for indicating that the SSH agent is not available.
18
18
  class AgentNotAvailable < AgentError; end
19
19
 
20
+ # Raised when the agent closes the connection before replying.
21
+ class AgentClosedConnection < AgentError; end
22
+
20
23
  # This class implements a simple client for the ssh-agent protocol. It
21
24
  # does not implement any specific protocol, but instead copies the
22
25
  # behavior of the ssh-agent functions in the OpenSSH library (3.8).
@@ -83,12 +86,18 @@ module Net
83
86
  # socket reports that it is an SSH2-compatible agent, this will fail
84
87
  # (it only supports the ssh-agent distributed by OpenSSH).
85
88
  def connect!(agent_socket_factory = nil, identity_agent = nil)
89
+ @agent_socket_factory = agent_socket_factory
90
+ @identity_agent = identity_agent
91
+ open_agent_socket!
92
+ end
93
+
94
+ def open_agent_socket!
86
95
  debug { "connecting to ssh-agent" }
87
96
  @socket =
88
- if agent_socket_factory
89
- agent_socket_factory.call
90
- elsif identity_agent
91
- unix_socket_class.open(File.expand_path(identity_agent))
97
+ if @agent_socket_factory
98
+ @agent_socket_factory.call
99
+ elsif @identity_agent
100
+ unix_socket_class.open(File.expand_path(@identity_agent))
92
101
  elsif ENV['SSH_AUTH_SOCK'] && unix_socket_class
93
102
  unix_socket_class.open(File.expand_path(ENV['SSH_AUTH_SOCK']))
94
103
  elsif Gem.win_platform? && RUBY_ENGINE != "jruby"
@@ -100,6 +109,7 @@ module Net
100
109
  error { "could not connect to ssh-agent: #{e.message}" }
101
110
  raise AgentNotAvailable, $!.message
102
111
  end
112
+ private :open_agent_socket!
103
113
 
104
114
  # Attempts to negotiate the SSH agent protocol version. Raises an error
105
115
  # if the version could not be negotiated successfully.
@@ -114,6 +124,11 @@ module Net
114
124
  elsif type != SSH_AGENT_RSA_IDENTITIES_ANSWER1 && type != SSH_AGENT_RSA_IDENTITIES_ANSWER2
115
125
  raise AgentNotAvailable, "unknown response from agent: #{type}, #{body.to_s.inspect}"
116
126
  end
127
+ rescue AgentClosedConnection
128
+ # Agents built on ssh-agent-lib <= 0.6.0 (e.g. older Proton Pass) drop the connection on
129
+ # this legacy request but handle the rest of the protocol (#1008).
130
+ debug { "ssh-agent closed the connection on the version request; assuming a modern agent and reconnecting" }
131
+ open_agent_socket!
117
132
  end
118
133
 
119
134
  # Return an array of all identities (public keys) known to the agent.
@@ -223,8 +238,14 @@ module Net
223
238
  # tuple consisting of the packet type, and the packet's body (which
224
239
  # is returned as a Net::SSH::Buffer).
225
240
  def read_packet
226
- buffer = Net::SSH::Buffer.new(@socket.read(4))
227
- buffer.append(@socket.read(buffer.read_long))
241
+ header = @socket.read(4)
242
+ raise AgentClosedConnection, "agent closed the connection" if header.nil?
243
+
244
+ buffer = Net::SSH::Buffer.new(header)
245
+ body = @socket.read(buffer.read_long)
246
+ raise AgentClosedConnection, "agent closed the connection" if body.nil?
247
+
248
+ buffer.append(body)
228
249
  type = buffer.read_byte
229
250
  debug { "received agent packet #{type} len #{buffer.length - 4}" }
230
251
  return type, buffer
@@ -59,6 +59,9 @@ module Net
59
59
  MEND = "-----END OPENSSH PRIVATE KEY-----"
60
60
  MAGIC = "openssh-key-v1"
61
61
 
62
+ # Same limit as OpenSSH; bcrypt_pbkdf with 2**32 rounds would run for months (GHSA-rpq3-v334-f5pm).
63
+ MAX_KDF_ROUNDS = 1 << 20
64
+
62
65
  class DecryptError < ArgumentError
63
66
  def initialize(message, encrypted_key: false)
64
67
  super(message)
@@ -109,6 +112,7 @@ module Net
109
112
  if kdfname == 'bcrypt'
110
113
  salt = kdfopts.read_string
111
114
  rounds = kdfopts.read_long
115
+ raise ArgumentError.new("Private key bcrypt rounds #{rounds} exceeds #{MAX_KDF_ROUNDS}") if rounds > MAX_KDF_ROUNDS
112
116
 
113
117
  key = bcrypt_pbkdf_key(password, salt, keylen + ivlen, rounds)
114
118
  else
@@ -145,6 +149,7 @@ module Net
145
149
  rescue LoadError
146
150
  raise DecryptError.new("bcrypt_pbkdf is required to decrypt bcrypt-encrypted OpenSSH private keys")
147
151
  end
152
+ raise DecryptError.new("Passphrase required for encrypted private key", encrypted_key: true) if password.nil? || password.empty?
148
153
 
149
154
  key = BCryptPbkdf::key(password, salt, length, rounds)
150
155
  raise DecryptError.new("BCryptPbkdf failed", encrypted_key: true) unless key
@@ -88,11 +88,8 @@ module Net
88
88
  IO.foreach(file) do |line|
89
89
  next if line =~ /^\s*(?:#.*)?$/
90
90
 
91
- if line =~ /^\s*(\S+)\s*=(.*)$/
92
- key, value = $1, $2
93
- else
94
- key, value = line.strip.split(/\s+/, 2)
95
- end
91
+ # As in OpenSSH, the keyword ends at the first whitespace or "=".
92
+ key, value = line.strip.split(/\s*=\s*|\s+/, 2)
96
93
 
97
94
  # silently ignore malformed entries
98
95
  next if value.nil?
@@ -152,9 +149,11 @@ module Net
152
149
  end
153
150
 
154
151
  # ProxyCommand and ProxyJump override each other so they need to be tracked togeather
155
- %w[proxyjump proxycommand].each do |proxy_key|
156
- if (proxy_value = settings.delete(proxy_key))
157
- settings['proxy'] ||= [proxy_key, proxy_value]
152
+ [globals, settings].each do |hash|
153
+ %w[proxyjump proxycommand].each do |proxy_key|
154
+ if (proxy_value = hash.delete(proxy_key))
155
+ hash['proxy'] ||= [proxy_key, proxy_value]
156
+ end
158
157
  end
159
158
  end
160
159
  end
@@ -302,13 +301,15 @@ module Net
302
301
  def setup_proxy(type, value)
303
302
  case type
304
303
  when 'proxycommand'
305
- if value !~ /^none$/
304
+ if value !~ /^none$/i
306
305
  require 'net/ssh/proxy/command'
307
306
  Net::SSH::Proxy::Command.new(value)
308
307
  end
309
308
  when 'proxyjump'
310
- require 'net/ssh/proxy/jump'
311
- Net::SSH::Proxy::Jump.new(value)
309
+ if value !~ /^none$/i
310
+ require 'net/ssh/proxy/jump'
311
+ Net::SSH::Proxy::Jump.new(value)
312
+ end
312
313
  end
313
314
  end
314
315
 
@@ -55,7 +55,7 @@ module Net
55
55
  prompter = nil
56
56
  result =
57
57
  begin
58
- key_type.read(data, passphrase || 'invalid')
58
+ key_type.read(data, passphrase)
59
59
  rescue *key_type.error_classes => e
60
60
  encrypted_key = !!key_type.encrypted_key?(data, e) if encrypted_key.nil?
61
61
  if encrypted_key && ask_passphrase
@@ -138,8 +138,9 @@ module Net
138
138
  raise Exception, "TODO: subclasses should implement"
139
139
  end
140
140
 
141
+ # A nil passphrase would make OpenSSL prompt on the terminal itself.
141
142
  def self.read(key_data, passphrase)
142
- open_ssl_class.new(key_data, passphrase)
143
+ open_ssl_class.new(key_data, passphrase || 'invalid')
143
144
  end
144
145
 
145
146
  def self.encrypted_key?(key_data, error)
@@ -149,7 +150,7 @@ module Net
149
150
 
150
151
  class OpenSSLPKeyType < OpenSSLKeyTypeBase
151
152
  def self.read(key_data, passphrase)
152
- open_ssl_class.read(key_data, passphrase)
153
+ open_ssl_class.read(key_data, passphrase || 'invalid')
153
154
  end
154
155
 
155
156
  def self.open_ssl_class
@@ -16,6 +16,11 @@ module Net
16
16
  # ...
17
17
  # end
18
18
  class Command
19
+ # The command line is run by the shell, so like OpenSSH 9.6 (CVE-2023-51385) we refuse
20
+ # host and user names containing shell metacharacters rather than substituting them.
21
+ INVALID_HOST_NAME = /\A-|[[:space:][:cntrl:]'`"$\\;&<>|(){},]/.freeze
22
+ INVALID_USER_NAME = /\A-|[[:cntrl:]'`";&<>|(){}]|[[:space:]]-|\\\z/.freeze
23
+
19
24
  # The command line template
20
25
  attr_reader :command_line_template
21
26
 
@@ -41,13 +46,13 @@ module Net
41
46
  command_line = @command_line_template.gsub(/%(.)/) {
42
47
  case $1
43
48
  when 'h'
44
- host
49
+ validate_host_name!(host)
45
50
  when 'p'
46
- port.to_s
51
+ validate_port!(port)
47
52
  when 'r'
48
53
  remote_user = connection_options && connection_options[:remote_user]
49
54
  if remote_user
50
- remote_user
55
+ validate_user_name!(remote_user)
51
56
  else
52
57
  raise ArgumentError, "remote user name not available"
53
58
  end
@@ -117,6 +122,26 @@ module Net
117
122
  Process.kill('TERM', io.pid)
118
123
  Thread.new { io.close }
119
124
  end
125
+
126
+ private
127
+
128
+ def validate_host_name!(host)
129
+ raise ArgumentError, "host name contains invalid characters: #{host.inspect}" if host.to_s.match?(INVALID_HOST_NAME)
130
+
131
+ host
132
+ end
133
+
134
+ def validate_user_name!(user)
135
+ raise ArgumentError, "user name contains invalid characters: #{user.inspect}" if user.to_s.match?(INVALID_USER_NAME)
136
+
137
+ user
138
+ end
139
+
140
+ def validate_port!(port)
141
+ raise ArgumentError, "port is not a number: #{port.inspect}" unless port.to_s.match?(/\A\d+\z/)
142
+
143
+ port.to_s
144
+ end
120
145
  end
121
146
  end
122
147
  end
@@ -1,3 +1,4 @@
1
+ require 'shellwords'
1
2
  require 'uri'
2
3
  require 'net/ssh/proxy/command'
3
4
 
@@ -34,20 +35,29 @@ module Net
34
35
  # We cannot build the ProxyCommand template until we know if the :config
35
36
  # option was specified during `Net::SSH.start`.
36
37
  def build_proxy_command_equivalent(connection_options = nil)
37
- first_jump, extra_jumps = jump_proxies.split(",", 2)
38
+ extra_jumps = jump_proxies.split(",", 2)[1]
38
39
  config = connection_options && connection_options[:config]
39
- uri = URI.parse("ssh://#{first_jump}")
40
+ uri, = jump_proxies.split(",").map { |jump| validate_jump!(jump) }
40
41
 
41
42
  template = "ssh".dup
42
43
  template << " -l #{uri.user}" if uri.user
43
44
  template << " -p #{uri.port}" if uri.port
44
45
  template << " -J #{extra_jumps}" if extra_jumps
45
- template << " -F #{config}" if config != true && config
46
+ template << " -F #{Shellwords.escape(config)}" if config != true && config
46
47
  template << " -W %h:%p "
47
48
  template << uri.host
48
49
 
49
50
  @command_line_template = template
50
51
  end
52
+
53
+ private
54
+
55
+ def validate_jump!(jump)
56
+ uri = URI.parse("ssh://#{jump}")
57
+ validate_user_name!(uri.user) if uri.user
58
+ validate_host_name!(uri.host)
59
+ uri
60
+ end
51
61
  end
52
62
  end
53
63
  end
@@ -405,6 +405,12 @@ module Net
405
405
 
406
406
  # The callback used when an auth-agent channel is requested by the server.
407
407
  def auth_agent_channel(session, channel, packet)
408
+ # Like OpenSSH, only relay the local agent when forwarding was requested (GHSA-qpg5-fx6p-8cq3).
409
+ unless session.options[:forward_agent] || @agent_forwarded
410
+ error { "server tried agent forwarding, which was not requested; refusing" }
411
+ raise Net::SSH::ChannelOpenFailed.new(1, "agent forwarding was not requested")
412
+ end
413
+
408
414
  info { "opening auth-agent channel" }
409
415
  channel[:invisible] = true
410
416
 
@@ -15,6 +15,9 @@ module Net
15
15
  module PacketStream # rubocop:disable Metrics/ModuleLength
16
16
  PROXY_COMMAND_HOST_IP = '<no hostip for proxy command>'.freeze
17
17
 
18
+ # Same limit as OpenSSH; a larger length would make us buffer whatever the server streams (GHSA-pfqh-p9vh-wx4p).
19
+ PACKET_MAX_SIZE = 256 * 1024
20
+
18
21
  include BufferedIo
19
22
 
20
23
  def self.extended(object)
@@ -187,6 +190,7 @@ module Net
187
190
  def cleanup
188
191
  client.cleanup
189
192
  server.cleanup
193
+ hang_up_proxy_command
190
194
  end
191
195
 
192
196
  # If the IO object requires a rekey operation (as indicated by either its
@@ -202,6 +206,17 @@ module Net
202
206
 
203
207
  protected
204
208
 
209
+ # Like OpenSSH, hang up a ProxyCommand child: tunnels such as EC2 Instance
210
+ # Connect don't exit on their own, and IO#close waits for them.
211
+ def hang_up_proxy_command
212
+ return unless respond_to?(:pid)
213
+
214
+ child = pid
215
+ Process.kill('HUP', child) if child
216
+ rescue IOError, Errno::ESRCH
217
+ # already closed, or the child has already been reaped
218
+ end
219
+
205
220
  # Called when this module is used to extend an object. It initializes
206
221
  # the states and generally prepares the object for use as a packet stream.
207
222
  def initialize_ssh
@@ -240,6 +255,7 @@ module Net
240
255
  @packet = Net::SSH::Buffer.new(server.update_cipher(data))
241
256
  @packet_length = @packet.read_long
242
257
  end
258
+ raise Net::SSH::Exception, "bad packet length #{@packet_length}" if @packet_length < 5 || @packet_length > PACKET_MAX_SIZE
243
259
  end
244
260
 
245
261
  need = @packet_length + 4 - aad_length - server.block_size
@@ -18,6 +18,10 @@ module Net
18
18
  # The SSH version string as reported by Net::SSH
19
19
  PROTO_VERSION = "SSH-2.0-Ruby/Net::SSH_#{Net::SSH::Version::CURRENT} #{RUBY_PLATFORM}"
20
20
 
21
+ # Same limits as OpenSSH, so a server can't make us buffer its banner without bound (GHSA-r75w-7fhp-84w8).
22
+ MAX_LINE_LENGTH = 8192
23
+ MAX_HEADER_LINES = 1024
24
+
21
25
  # Any header text sent by the server prior to sending the version.
22
26
  attr_reader :header
23
27
 
@@ -45,24 +49,15 @@ module Net
45
49
  socket.write "#{PROTO_VERSION}\r\n"
46
50
  socket.flush
47
51
 
48
- raise Net::SSH::ConnectionTimeout, "timeout during server version negotiating" if timeout && !IO.select([socket], nil, nil, timeout)
52
+ deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
49
53
 
50
- loop do
51
- @version = String.new
52
- loop do
53
- begin
54
- b = socket.readpartial(1)
55
- raise Net::SSH::Disconnect, "connection closed by remote host" if b.nil?
56
- rescue EOFError
57
- raise Net::SSH::Disconnect, "connection closed by remote host"
58
- end
59
- @version << b
60
- break if b == "\n"
61
- end
54
+ MAX_HEADER_LINES.times do
55
+ @version = read_line(socket, deadline)
62
56
  break if @version.match(/^SSH-/)
63
57
 
64
58
  @header << @version
65
59
  end
60
+ raise Net::SSH::Exception, "no SSH version received in the first #{MAX_HEADER_LINES} lines" unless @version.match(/^SSH-/)
66
61
 
67
62
  @version.chomp!
68
63
  debug { "remote is `#{@version}'" }
@@ -71,6 +66,31 @@ module Net
71
66
 
72
67
  raise Net::SSH::ConnectionTimeout, "timeout during client version negotiating" if timeout && !IO.select(nil, [socket], nil, timeout)
73
68
  end
69
+
70
+ def read_line(socket, deadline)
71
+ line = String.new
72
+ loop do
73
+ wait_readable(socket, deadline)
74
+ begin
75
+ b = socket.readpartial(1)
76
+ raise Net::SSH::Disconnect, "connection closed by remote host" if b.nil?
77
+ rescue EOFError
78
+ raise Net::SSH::Disconnect, "connection closed by remote host"
79
+ end
80
+ line << b
81
+ return line if b == "\n"
82
+ raise Net::SSH::Exception, "server version line exceeds #{MAX_LINE_LENGTH} bytes" if line.bytesize > MAX_LINE_LENGTH
83
+ end
84
+ end
85
+
86
+ def wait_readable(socket, deadline)
87
+ return unless deadline
88
+
89
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
90
+ return if remaining > 0 && IO.select([socket], nil, nil, remaining)
91
+
92
+ raise Net::SSH::ConnectionTimeout, "timeout during server version negotiating"
93
+ end
74
94
  end
75
95
  end
76
96
  end
@@ -27,6 +27,10 @@ module Net
27
27
  # The standard port for the SSH protocol.
28
28
  DEFAULT_PORT = 22
29
29
 
30
+ # Packets held back during a re-key are bounded so a server can't exhaust memory (GHSA-ggxh-cfwq-5xqc).
31
+ MAX_QUEUED_PACKETS = 16_384
32
+ MAX_QUEUED_BYTES = 32 * 1024 * 1024
33
+
30
34
  # The host to connect to, as given to the constructor.
31
35
  attr_reader :host
32
36
 
@@ -81,6 +85,7 @@ module Net
81
85
  debug { "connection established" }
82
86
 
83
87
  @queue = []
88
+ @queued_bytes = 0
84
89
 
85
90
  @server_version = ServerVersion.new(socket, logger, options[:timeout])
86
91
 
@@ -192,7 +197,7 @@ module Net
192
197
  # read from the queue before the socket is queried.
193
198
  def poll_message(mode = :nonblock, consume_queue = true)
194
199
  loop do
195
- return @queue.shift if consume_queue && @queue.any? && algorithms.allow?(@queue.first)
200
+ return shift_queue if consume_queue && @queue.any? && algorithms.allow?(@queue.first)
196
201
 
197
202
  packet = socket.next_packet(mode, options[:timeout])
198
203
  return nil if packet.nil?
@@ -219,6 +224,7 @@ module Net
219
224
 
220
225
  else
221
226
  return packet if algorithms.allow?(packet)
227
+ raise Net::SSH::Exception, "unexpected packet type #{packet.type} during initial key exchange" unless algorithms.initialized?
222
228
 
223
229
  push(packet)
224
230
  end
@@ -242,6 +248,11 @@ module Net
242
248
  # #poll_message will return packets from the queue in the order they
243
249
  # were received.
244
250
  def push(packet)
251
+ @queued_bytes += packet.length
252
+ if @queue.size >= MAX_QUEUED_PACKETS || @queued_bytes > MAX_QUEUED_BYTES
253
+ raise Net::SSH::Exception, "too many packets queued during key exchange"
254
+ end
255
+
245
256
  @queue.push(packet)
246
257
  end
247
258
 
@@ -284,6 +295,12 @@ module Net
284
295
 
285
296
  private
286
297
 
298
+ def shift_queue
299
+ packet = @queue.shift
300
+ @queued_bytes -= packet.length
301
+ packet
302
+ end
303
+
287
304
  # Turns Nagle's algorithm off on the socket, so that a packet written
288
305
  # while the previous one is still unacknowledged goes out right away
289
306
  # instead of waiting for the server's ACK. A ProxyCommand hands the
@@ -51,6 +51,9 @@ module Net
51
51
  # implement them (Net::SSH::HostKeyEntries::CertAuthority). Non-certificate
52
52
  # entries do not respond to these predicates and are left untouched.
53
53
  def verify_certificate!(entry, host_keys, arguments)
54
+ # Plain keys carry no certificate constraints, and respond_to? on this delegator prints a deprecation warning (#1026).
55
+ return if entry.is_a?(Net::SSH::HostKeyEntries::PubKey)
56
+
54
57
  cert = arguments[:key]
55
58
 
56
59
  if entry.respond_to?(:matches_validity?) && !entry.matches_validity?(cert)
@@ -56,7 +56,7 @@ module Net
56
56
 
57
57
  # The prerelease component of this version of the Net::SSH library
58
58
  # nil allowed
59
- PRE = "rc1"
59
+ PRE = "rc3"
60
60
 
61
61
  # The current version of the Net::SSH library as a Version instance
62
62
  CURRENT = new(*[MAJOR, MINOR, TINY, PRE].compact)
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-ssh
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.0.rc1
4
+ version: 8.0.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamis Buck
@@ -32,7 +32,7 @@ cert_chain:
32
32
  ufHf4AX2UIkJbh7zCPkiNCqIr7MSWLNFG/9lOlHYsEJM8XujT1ofPobYx6YSFx/C
33
33
  7HBrI7UX7awt6UvBZebhcHzyMHxg/B5PVQllPA==
34
34
  -----END CERTIFICATE-----
35
- date: 2026-10-04 00:00:00.000000000 Z
35
+ date: 2026-10-11 00:00:00.000000000 Z
36
36
  dependencies:
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: logger
metadata.gz.sig CHANGED
Binary file