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 +4 -4
- checksums.yaml.gz.sig +1 -1
- data/.github/workflows/rubocop.yml +3 -1
- data/CHANGES.txt +17 -0
- data/lib/net/ssh/authentication/agent.rb +27 -6
- data/lib/net/ssh/authentication/ed25519.rb +5 -0
- data/lib/net/ssh/config.rb +12 -11
- data/lib/net/ssh/key_factory.rb +4 -3
- data/lib/net/ssh/proxy/command.rb +28 -3
- data/lib/net/ssh/proxy/jump.rb +13 -3
- data/lib/net/ssh/service/forward.rb +6 -0
- data/lib/net/ssh/transport/packet_stream.rb +16 -0
- data/lib/net/ssh/transport/server_version.rb +33 -13
- data/lib/net/ssh/transport/session.rb +18 -1
- data/lib/net/ssh/verifiers/always.rb +3 -0
- data/lib/net/ssh/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +2 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 03f0ed4c48532bcfa337a176d655407155043de284d1428cd51a4bf2b7cb2f58
|
|
4
|
+
data.tar.gz: 49e8bbef56ac3139f5bb05cfad70afb33dda44227e9ce17f4424bd5aed749b95
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 56ec747abdff1eb6a0cbfb03adb3f880246372afe864f421c4241881671b9ce49dc1dc37a30c9e75e52526284b7c05caab1f14d367f94a1cdd422e1cf4aea93d
|
|
7
|
+
data.tar.gz: df092fc1f9da4c85eaf3b305890e0635b9d16d72b3bf05711180a9c28652d97a987be3011bbf7610363d0a9412dafa056c527172519a92c2bf77e7f228bcade8
|
checksums.yaml.gz.sig
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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����
|
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
|
-
|
|
227
|
-
|
|
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
|
data/lib/net/ssh/config.rb
CHANGED
|
@@ -88,11 +88,8 @@ module Net
|
|
|
88
88
|
IO.foreach(file) do |line|
|
|
89
89
|
next if line =~ /^\s*(?:#.*)?$/
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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
|
|
data/lib/net/ssh/key_factory.rb
CHANGED
|
@@ -55,7 +55,7 @@ module Net
|
|
|
55
55
|
prompter = nil
|
|
56
56
|
result =
|
|
57
57
|
begin
|
|
58
|
-
key_type.read(data, passphrase
|
|
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
|
|
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
|
data/lib/net/ssh/proxy/jump.rb
CHANGED
|
@@ -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
|
-
|
|
38
|
+
extra_jumps = jump_proxies.split(",", 2)[1]
|
|
38
39
|
config = connection_options && connection_options[:config]
|
|
39
|
-
uri =
|
|
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
|
-
|
|
52
|
+
deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
@version =
|
|
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
|
|
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)
|
data/lib/net/ssh/version.rb
CHANGED
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.
|
|
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-
|
|
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
|