net-ssh 7.3.4 → 7.3.5
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 +0 -0
- data/CHANGES.txt +9 -0
- data/lib/net/ssh/authentication/ed25519.rb +10 -0
- 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 +4 -0
- data/lib/net/ssh/transport/server_version.rb +33 -13
- data/lib/net/ssh/transport/session.rb +18 -1
- 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: 7d7e10bf0a8b8fd5369085ec9d379b11f88e980fa57586fa6af75f82c04f44e9
|
|
4
|
+
data.tar.gz: a1e5198b60b4209e00a7599f800aa993e588ddf0c501a3ebbfa8c75888909524
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff0336b092a4597e1676a82184efed7fdb1b0f1719d7615097ae430b502471e82e542db0fa0d75db0126bd7fe9c164981e4101dbb67bd19c5578c2d140b39c72
|
|
7
|
+
data.tar.gz: caad46ec0db79f1d9d273b1b2026d9811e53ed9bdcfac50a315749d48c73d5cfb9421f15e7f569b1d045cea6a67112f950ffb21ce16ed01d4154c52396cc35c9
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGES.txt
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
=== 7.3.5
|
|
2
|
+
|
|
3
|
+
* 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]
|
|
4
|
+
* 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]
|
|
5
|
+
* 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]
|
|
6
|
+
* 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]
|
|
7
|
+
* Security: reject packet lengths below 5 or above 256 KB, as OpenSSH does [GHSA-pfqh-p9vh-wx4p]
|
|
8
|
+
* 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]
|
|
9
|
+
|
|
1
10
|
=== 7.3.4
|
|
2
11
|
|
|
3
12
|
* Verify @cert-authority host certificate constraints — principals (shell-glob, matching OpenSSH), validity window, host type, and critical options — instead of trusting any CA-signed certificate for any host [#1010]
|
|
@@ -27,6 +27,9 @@ module Net
|
|
|
27
27
|
MEND = "-----END OPENSSH PRIVATE KEY-----"
|
|
28
28
|
MAGIC = "openssh-key-v1"
|
|
29
29
|
|
|
30
|
+
# Same limit as OpenSSH; bcrypt_pbkdf with 2**32 rounds would run for months (GHSA-rpq3-v334-f5pm).
|
|
31
|
+
MAX_KDF_ROUNDS = 1 << 20
|
|
32
|
+
|
|
30
33
|
class DecryptError < ArgumentError
|
|
31
34
|
def initialize(message, encrypted_key: false)
|
|
32
35
|
super(message)
|
|
@@ -38,6 +41,11 @@ module Net
|
|
|
38
41
|
end
|
|
39
42
|
end
|
|
40
43
|
|
|
44
|
+
def self.check_bcrypt_kdf!(rounds, password)
|
|
45
|
+
raise ArgumentError.new("Private key bcrypt rounds #{rounds} exceeds #{MAX_KDF_ROUNDS}") if rounds > MAX_KDF_ROUNDS
|
|
46
|
+
raise DecryptError.new("Passphrase required for encrypted private key", encrypted_key: true) if password.nil? || password.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
def self.read(datafull, password)
|
|
42
50
|
datafull = datafull.strip
|
|
43
51
|
raise ArgumentError.new("Expected #{MBEGIN} at start of private key") unless datafull.start_with?(MBEGIN)
|
|
@@ -74,6 +82,8 @@ module Net
|
|
|
74
82
|
|
|
75
83
|
raise "BCryptPbkdf is not implemented for jruby" if RUBY_PLATFORM == "java"
|
|
76
84
|
|
|
85
|
+
check_bcrypt_kdf!(rounds, password)
|
|
86
|
+
|
|
77
87
|
key = BCryptPbkdf::key(password, salt, keylen + ivlen, rounds)
|
|
78
88
|
raise DecryptError.new("BCyryptPbkdf failed", encrypted_key: true) unless key
|
|
79
89
|
else
|
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)
|
|
@@ -240,6 +243,7 @@ module Net
|
|
|
240
243
|
@packet = Net::SSH::Buffer.new(server.update_cipher(data))
|
|
241
244
|
@packet_length = @packet.read_long
|
|
242
245
|
end
|
|
246
|
+
raise Net::SSH::Exception, "bad packet length #{@packet_length}" if @packet_length < 5 || @packet_length > PACKET_MAX_SIZE
|
|
243
247
|
end
|
|
244
248
|
|
|
245
249
|
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
|
|
|
@@ -79,6 +83,7 @@ module Net
|
|
|
79
83
|
debug { "connection established" }
|
|
80
84
|
|
|
81
85
|
@queue = []
|
|
86
|
+
@queued_bytes = 0
|
|
82
87
|
|
|
83
88
|
@host_key_verifier = select_host_key_verifier(options[:verify_host_key])
|
|
84
89
|
|
|
@@ -188,7 +193,7 @@ module Net
|
|
|
188
193
|
# read from the queue before the socket is queried.
|
|
189
194
|
def poll_message(mode = :nonblock, consume_queue = true)
|
|
190
195
|
loop do
|
|
191
|
-
return
|
|
196
|
+
return shift_queue if consume_queue && @queue.any? && algorithms.allow?(@queue.first)
|
|
192
197
|
|
|
193
198
|
packet = socket.next_packet(mode, options[:timeout])
|
|
194
199
|
return nil if packet.nil?
|
|
@@ -211,6 +216,7 @@ module Net
|
|
|
211
216
|
|
|
212
217
|
else
|
|
213
218
|
return packet if algorithms.allow?(packet)
|
|
219
|
+
raise Net::SSH::Exception, "unexpected packet type #{packet.type} during initial key exchange" unless algorithms.initialized?
|
|
214
220
|
|
|
215
221
|
push(packet)
|
|
216
222
|
end
|
|
@@ -234,6 +240,11 @@ module Net
|
|
|
234
240
|
# #poll_message will return packets from the queue in the order they
|
|
235
241
|
# were received.
|
|
236
242
|
def push(packet)
|
|
243
|
+
@queued_bytes += packet.length
|
|
244
|
+
if @queue.size >= MAX_QUEUED_PACKETS || @queued_bytes > MAX_QUEUED_BYTES
|
|
245
|
+
raise Net::SSH::Exception, "too many packets queued during key exchange"
|
|
246
|
+
end
|
|
247
|
+
|
|
237
248
|
@queue.push(packet)
|
|
238
249
|
end
|
|
239
250
|
|
|
@@ -276,6 +287,12 @@ module Net
|
|
|
276
287
|
|
|
277
288
|
private
|
|
278
289
|
|
|
290
|
+
def shift_queue
|
|
291
|
+
packet = @queue.shift
|
|
292
|
+
@queued_bytes -= packet.length
|
|
293
|
+
packet
|
|
294
|
+
end
|
|
295
|
+
|
|
279
296
|
# Compatibility verifier which allows users to keep using
|
|
280
297
|
# custom verifier code without adding new :verify_signature
|
|
281
298
|
# method.
|
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: 7.3.
|
|
4
|
+
version: 7.3.5
|
|
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-05 00:00:00.000000000 Z
|
|
36
36
|
dependencies:
|
|
37
37
|
- !ruby/object:Gem::Dependency
|
|
38
38
|
name: bcrypt_pbkdf
|
metadata.gz.sig
CHANGED
|
Binary file
|