net-ssh 8.0.0.beta3 → 8.0.0.rc2
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/.github/workflows/ci.yml +1 -1
- data/CHANGES.txt +27 -0
- data/Dockerfile +3 -0
- data/Dockerfile.zlib_ng +1 -1
- data/Rakefile +18 -12
- data/lib/net/ssh/authentication/ed25519.rb +5 -0
- data/lib/net/ssh/authentication/key_manager.rb +3 -2
- data/lib/net/ssh/key_factory.rb +4 -3
- data/lib/net/ssh/prompt.rb +3 -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 +9 -3
- data/lib/net/ssh/transport/algorithms.rb +17 -10
- data/lib/net/ssh/transport/constants.rb +1 -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 +40 -3
- data/lib/net/ssh/version.rb +1 -1
- data/lib/net/ssh.rb +7 -1
- data/net-ssh.gemspec +1 -0
- data.tar.gz.sig +0 -0
- metadata +16 -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: cd6a3f2cc54c2b16873c1bcd92038d6c70e09581fd29c5a6674b708a76c9fec9
|
|
4
|
+
data.tar.gz: 80fd4270605a25552b4cf7c0ca0c73b358f3806bd3a6654d15b94682fce0ff81
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0cd7d51126432199154b9aeeaf28dd6f0df05c6673c4ccf580b27a84c54d6865500b6ff4e2814e9242cc7dc882d3eec7d95000856b5b732dab97d493d1eabe83
|
|
7
|
+
data.tar.gz: abce0bd3fcd5b47f74cc9fae9e684282e927b3af17c424e265f87aaf41fb85afbda631a588f080687804102a75692fa2086b8e82bda4af2d99ae824f2251b33d
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/.github/workflows/ci.yml
CHANGED
data/CHANGES.txt
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
1
|
+
=== 8.0.0 rc2
|
|
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
|
+
|
|
10
|
+
=== 8.0.0 rc1
|
|
11
|
+
|
|
12
|
+
* 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]
|
|
13
|
+
* Ruby 4.0 support: add `logger` as a runtime dependency; Windows Pageant users on Ruby 4.0 need `fiddle` in their Gemfile [#984, #1012]
|
|
14
|
+
* Close the socket when connection setup fails, e.g. on a server version timeout [#1013]
|
|
15
|
+
* Ignore SSH_MSG_EXT_INFO from servers that send it unrequested [#1020]
|
|
16
|
+
* Fix `undefined method 'to_pem'` for ECDSA key_data when an agent is running [#982]
|
|
17
|
+
* Agent forwarding connects to the agent set by `identity_agent`/IdentityAgent [#974]
|
|
18
|
+
* Keyboard-interactive prompts show input when the server asks for it to be echoed [#1014]
|
|
19
|
+
* Raise Net::SSH::Exception on a truncated KEXINIT [#1016]
|
|
20
|
+
* Remote forwarding no longer prints diagnostics to stdout [#953]
|
|
21
|
+
|
|
1
22
|
=== 8.0.0 beta3
|
|
2
23
|
|
|
3
24
|
* 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 [#1009]
|
|
4
25
|
|
|
26
|
+
=== 8.0.0 beta1
|
|
27
|
+
|
|
28
|
+
* BREAKING: require Ruby 2.7 and the openssl gem >= 3.2 [#1001]
|
|
29
|
+
* Use OpenSSL for Ed25519, X25519 and ChaCha20-Poly1305; the ed25519, x25519 and rbnacl gems are no longer needed [#1001]
|
|
30
|
+
* Support AEAD-encrypted OpenSSH private keys [#1001]
|
|
31
|
+
|
|
5
32
|
=== 7.3.3
|
|
6
33
|
|
|
7
34
|
* Support .pub file as IdentityFile to filter/prioritize agent keys (1Password SSH agent use case) [#942]
|
data/Dockerfile
CHANGED
|
@@ -3,6 +3,9 @@ FROM ruby:${RUBY_VERSION}
|
|
|
3
3
|
|
|
4
4
|
ARG BUNDLERV=
|
|
5
5
|
|
|
6
|
+
# Ruby 2.7 and 3.0 images are built on Debian bullseye, which only archive.debian.org still serves.
|
|
7
|
+
RUN if grep -q bullseye /etc/os-release; then sed -i 's|deb.debian.org|archive.debian.org|' /etc/apt/sources.list; fi
|
|
8
|
+
|
|
6
9
|
RUN apt update && apt install -y openssh-server sudo netcat-openbsd \
|
|
7
10
|
&& useradd --create-home --shell '/bin/bash' --comment 'NetSSH' 'net_ssh_1' \
|
|
8
11
|
&& useradd --create-home --shell '/bin/bash' --comment 'NetSSH' 'net_ssh_2' \
|
data/Dockerfile.zlib_ng
CHANGED
|
@@ -11,7 +11,7 @@ ENV INSTALL_PATH="/netssh"
|
|
|
11
11
|
# Ruby/OpenSSL initialises — setting it in the Rakefile is too late.
|
|
12
12
|
ENV OPENSSL_CONF="/netssh/test/openssl3.conf"
|
|
13
13
|
|
|
14
|
-
RUN dnf install -y openssl openssl-devel ruby ruby-devel git make gcc zlib-ng crypto-policies-scripts && \
|
|
14
|
+
RUN dnf install -y openssl openssl-devel ruby ruby-devel git make gcc redhat-rpm-config zlib-ng crypto-policies-scripts && \
|
|
15
15
|
update-crypto-policies --set LEGACY
|
|
16
16
|
|
|
17
17
|
WORKDIR $INSTALL_PATH
|
data/Rakefile
CHANGED
|
@@ -9,7 +9,11 @@ require "rake"
|
|
|
9
9
|
require "rake/clean"
|
|
10
10
|
require "bundler/gem_tasks"
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
begin
|
|
13
|
+
require "rdoc/task"
|
|
14
|
+
rescue LoadError
|
|
15
|
+
# rdoc is no longer a default gem as of Ruby 4.0; the rest of the Rakefile works without it.
|
|
16
|
+
end
|
|
13
17
|
|
|
14
18
|
desc "When releasing make sure NET_SSH_BUILDGEM_SIGNED is set"
|
|
15
19
|
task :check_NET_SSH_BUILDGEM_SIGNED do
|
|
@@ -27,17 +31,19 @@ require_relative "lib/net/ssh/version"
|
|
|
27
31
|
version = Net::SSH::Version::CURRENT
|
|
28
32
|
|
|
29
33
|
extra_files = %w[LICENSE.txt THANKS.txt CHANGES.txt]
|
|
30
|
-
RDoc::Task
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
if defined?(RDoc::Task)
|
|
35
|
+
RDoc::Task.new do |rdoc|
|
|
36
|
+
rdoc.rdoc_dir = "rdoc"
|
|
37
|
+
rdoc.title = "#{name} #{version}"
|
|
38
|
+
rdoc.generator = 'hanna' # gem install hanna-nouveau
|
|
39
|
+
rdoc.main = 'README.md'
|
|
40
|
+
rdoc.rdoc_files.include("README*")
|
|
41
|
+
rdoc.rdoc_files.include("bin/*.rb")
|
|
42
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
|
43
|
+
extra_files.each { |file|
|
|
44
|
+
rdoc.rdoc_files.include(file) if File.exist?(file)
|
|
45
|
+
}
|
|
46
|
+
end
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
namespace :cert do
|
|
@@ -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
|
|
@@ -127,9 +127,10 @@ module Net
|
|
|
127
127
|
if agent
|
|
128
128
|
agent.identities.each do |key|
|
|
129
129
|
corresponding_user_identity = user_identities.detect { |identity|
|
|
130
|
-
identity[:public_key] && identity[:public_key].
|
|
130
|
+
identity[:public_key] && identity[:public_key].to_blob == key.to_blob
|
|
131
131
|
}
|
|
132
|
-
|
|
132
|
+
# By identity: EC::Point#== raises TypeError when compared with other key types.
|
|
133
|
+
user_identities.reject! { |identity| identity.equal?(corresponding_user_identity) }
|
|
133
134
|
|
|
134
135
|
if !options[:keys_only] || corresponding_user_identity
|
|
135
136
|
known_identities[key] = { from: :agent, identity: key }
|
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
|
data/lib/net/ssh/prompt.rb
CHANGED
|
@@ -41,9 +41,9 @@ module Net
|
|
|
41
41
|
def ask(prompt, echo = true)
|
|
42
42
|
$stdout.print(prompt)
|
|
43
43
|
$stdout.flush
|
|
44
|
-
ret = $stdin.noecho(&:gets)
|
|
45
|
-
$stdout.print("\n")
|
|
46
|
-
ret
|
|
44
|
+
ret = echo ? $stdin.gets : $stdin.noecho(&:gets)
|
|
45
|
+
$stdout.print("\n") unless echo
|
|
46
|
+
ret.chomp
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
# success method will be called when the password was accepted
|
|
@@ -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
|
|
@@ -388,13 +388,13 @@ module Net
|
|
|
388
388
|
originator_address = packet.read_string
|
|
389
389
|
originator_port = packet.read_long
|
|
390
390
|
|
|
391
|
-
|
|
391
|
+
debug { "REMOTE 0: #{connected_port} #{connected_address} #{originator_address} #{originator_port}" }
|
|
392
392
|
remote = @remote_forwarded_ports[[connected_port, connected_address]]
|
|
393
393
|
if remote.nil?
|
|
394
394
|
raise Net::SSH::ChannelOpenFailed.new(1, "unknown request from remote forwarded connection on #{connected_address}:#{connected_port}")
|
|
395
395
|
end
|
|
396
396
|
|
|
397
|
-
|
|
397
|
+
debug { "REMOTE: #{remote.host} #{remote.port}" }
|
|
398
398
|
client = TCPSocket.new(remote.host, remote.port)
|
|
399
399
|
info { "connected #{connected_address}:#{connected_port} originator #{originator_address}:#{originator_port}" }
|
|
400
400
|
|
|
@@ -405,11 +405,17 @@ 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
|
|
|
411
417
|
begin
|
|
412
|
-
agent = Authentication::Agent.connect(logger, session.options[:agent_socket_factory])
|
|
418
|
+
agent = Authentication::Agent.connect(logger, session.options[:agent_socket_factory], session.options[:identity_agent])
|
|
413
419
|
if (agent.socket.is_a? ::IO)
|
|
414
420
|
prepare_client(agent.socket, channel, :agent)
|
|
415
421
|
else
|
|
@@ -347,16 +347,16 @@ module Net
|
|
|
347
347
|
|
|
348
348
|
packet.read(16) # skip the cookie value
|
|
349
349
|
|
|
350
|
-
data[:kex] = packet
|
|
351
|
-
data[:host_key] = packet
|
|
352
|
-
data[:encryption_client] = packet
|
|
353
|
-
data[:encryption_server] = packet
|
|
354
|
-
data[:hmac_client] = packet
|
|
355
|
-
data[:hmac_server] = packet
|
|
356
|
-
data[:compression_client] = packet
|
|
357
|
-
data[:compression_server] = packet
|
|
358
|
-
data[:language_client] = packet
|
|
359
|
-
data[:language_server] = packet
|
|
350
|
+
data[:kex] = read_name_list(packet)
|
|
351
|
+
data[:host_key] = read_name_list(packet)
|
|
352
|
+
data[:encryption_client] = read_name_list(packet)
|
|
353
|
+
data[:encryption_server] = read_name_list(packet)
|
|
354
|
+
data[:hmac_client] = read_name_list(packet)
|
|
355
|
+
data[:hmac_server] = read_name_list(packet)
|
|
356
|
+
data[:compression_client] = read_name_list(packet)
|
|
357
|
+
data[:compression_server] = read_name_list(packet)
|
|
358
|
+
data[:language_client] = read_name_list(packet)
|
|
359
|
+
data[:language_server] = read_name_list(packet)
|
|
360
360
|
|
|
361
361
|
# TODO: if first_kex_packet_follows, we need to try to skip the
|
|
362
362
|
# actual kexinit stuff and try to guess what the server is doing...
|
|
@@ -366,6 +366,13 @@ module Net
|
|
|
366
366
|
return data
|
|
367
367
|
end
|
|
368
368
|
|
|
369
|
+
# Reads one comma-separated algorithm name-list from a KEXINIT packet.
|
|
370
|
+
def read_name_list(packet)
|
|
371
|
+
list = packet.read_string or raise Net::SSH::Exception, "malformed KEXINIT packet from server"
|
|
372
|
+
|
|
373
|
+
list.split(/,/)
|
|
374
|
+
end
|
|
375
|
+
|
|
369
376
|
# Given the #algorithms map of preferred algorithm types, this constructs
|
|
370
377
|
# a KEXINIT packet to send to the server. It does not actually send it,
|
|
371
378
|
# it simply builds the packet and returns it.
|
|
@@ -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
|
|
|
@@ -62,6 +66,7 @@ module Net
|
|
|
62
66
|
@port = options[:port] || DEFAULT_PORT
|
|
63
67
|
@bind_address = options[:bind_address] || nil
|
|
64
68
|
@options = options
|
|
69
|
+
@host_key_verifier = select_host_key_verifier(options[:verify_host_key])
|
|
65
70
|
|
|
66
71
|
@socket =
|
|
67
72
|
if (factory = options[:proxy])
|
|
@@ -75,12 +80,12 @@ module Net
|
|
|
75
80
|
|
|
76
81
|
@socket.extend(PacketStream)
|
|
77
82
|
@socket.logger = @logger
|
|
83
|
+
set_no_delay unless options[:no_delay] == false
|
|
78
84
|
|
|
79
85
|
debug { "connection established" }
|
|
80
86
|
|
|
81
87
|
@queue = []
|
|
82
|
-
|
|
83
|
-
@host_key_verifier = select_host_key_verifier(options[:verify_host_key])
|
|
88
|
+
@queued_bytes = 0
|
|
84
89
|
|
|
85
90
|
@server_version = ServerVersion.new(socket, logger, options[:timeout])
|
|
86
91
|
|
|
@@ -88,7 +93,11 @@ module Net
|
|
|
88
93
|
@algorithms.start
|
|
89
94
|
wait { algorithms.initialized? }
|
|
90
95
|
rescue Errno::ETIMEDOUT
|
|
96
|
+
@socket&.close
|
|
91
97
|
raise Net::SSH::ConnectionTimeout
|
|
98
|
+
rescue StandardError
|
|
99
|
+
@socket&.close
|
|
100
|
+
raise
|
|
92
101
|
end
|
|
93
102
|
|
|
94
103
|
def host_keys
|
|
@@ -188,7 +197,7 @@ module Net
|
|
|
188
197
|
# read from the queue before the socket is queried.
|
|
189
198
|
def poll_message(mode = :nonblock, consume_queue = true)
|
|
190
199
|
loop do
|
|
191
|
-
return
|
|
200
|
+
return shift_queue if consume_queue && @queue.any? && algorithms.allow?(@queue.first)
|
|
192
201
|
|
|
193
202
|
packet = socket.next_packet(mode, options[:timeout])
|
|
194
203
|
return nil if packet.nil?
|
|
@@ -206,11 +215,16 @@ module Net
|
|
|
206
215
|
when DEBUG
|
|
207
216
|
send(packet[:always_display] ? :fatal : :debug) { packet[:message] }
|
|
208
217
|
|
|
218
|
+
# We don't advertise ext-info-c, but some servers send EXT_INFO anyway (#955).
|
|
219
|
+
when EXT_INFO
|
|
220
|
+
debug { "ignoring EXT_INFO packet" }
|
|
221
|
+
|
|
209
222
|
when KEXINIT
|
|
210
223
|
algorithms.accept_kexinit(packet)
|
|
211
224
|
|
|
212
225
|
else
|
|
213
226
|
return packet if algorithms.allow?(packet)
|
|
227
|
+
raise Net::SSH::Exception, "unexpected packet type #{packet.type} during initial key exchange" unless algorithms.initialized?
|
|
214
228
|
|
|
215
229
|
push(packet)
|
|
216
230
|
end
|
|
@@ -234,6 +248,11 @@ module Net
|
|
|
234
248
|
# #poll_message will return packets from the queue in the order they
|
|
235
249
|
# were received.
|
|
236
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
|
+
|
|
237
256
|
@queue.push(packet)
|
|
238
257
|
end
|
|
239
258
|
|
|
@@ -276,6 +295,24 @@ module Net
|
|
|
276
295
|
|
|
277
296
|
private
|
|
278
297
|
|
|
298
|
+
def shift_queue
|
|
299
|
+
packet = @queue.shift
|
|
300
|
+
@queued_bytes -= packet.length
|
|
301
|
+
packet
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Turns Nagle's algorithm off on the socket, so that a packet written
|
|
305
|
+
# while the previous one is still unacknowledged goes out right away
|
|
306
|
+
# instead of waiting for the server's ACK. A ProxyCommand hands the
|
|
307
|
+
# transport a pipe rather than a socket, and the command on its other
|
|
308
|
+
# end owns the TCP connection, so there is nothing to set in that case.
|
|
309
|
+
def set_no_delay
|
|
310
|
+
return unless socket.respond_to?(:setsockopt)
|
|
311
|
+
|
|
312
|
+
debug { "setting TCP_NODELAY" }
|
|
313
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
314
|
+
end
|
|
315
|
+
|
|
279
316
|
# Compatibility verifier which allows users to keep using
|
|
280
317
|
# custom verifier code without adding new :verify_signature
|
|
281
318
|
# method.
|
data/lib/net/ssh/version.rb
CHANGED
data/lib/net/ssh.rb
CHANGED
|
@@ -69,7 +69,7 @@ module Net
|
|
|
69
69
|
max_win_size send_env set_env use_agent number_of_password_prompts
|
|
70
70
|
append_all_supported_algorithms non_interactive password_prompt
|
|
71
71
|
agent_socket_factory minimum_dh_bits verify_host_key
|
|
72
|
-
fingerprint_hash check_host_ip pubkey_algorithms
|
|
72
|
+
fingerprint_hash check_host_ip pubkey_algorithms no_delay
|
|
73
73
|
]
|
|
74
74
|
|
|
75
75
|
# The standard means of starting a new SSH connection. When used with a
|
|
@@ -156,6 +156,12 @@ module Net
|
|
|
156
156
|
# for better performance if your SSH server supports it (most do).
|
|
157
157
|
# * :max_win_size => maximum size we tell the other side that is supported for
|
|
158
158
|
# the window.
|
|
159
|
+
# * :no_delay => turns Nagle's algorithm off on the connection's socket
|
|
160
|
+
# (TCP_NODELAY), so a packet written while the previous one is still
|
|
161
|
+
# unacknowledged is sent right away instead of waiting for the server's
|
|
162
|
+
# ACK. OpenSSH does the same for every connection. Set to +false+ to keep
|
|
163
|
+
# Nagle's algorithm on. Has no effect when the socket comes from a
|
|
164
|
+
# ProxyCommand, whose command owns the TCP connection. Defaults to +true+.
|
|
159
165
|
# * :non_interactive => set to true if your app is non interactive and prefers
|
|
160
166
|
# authentication failure vs password prompt. Non-interactive applications
|
|
161
167
|
# should set it to true to prefer failing a password/etc auth methods vs.
|
data/net-ssh.gemspec
CHANGED
|
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
|
|
|
30
30
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
31
31
|
spec.require_paths = ["lib"]
|
|
32
32
|
|
|
33
|
+
spec.add_dependency("logger")
|
|
33
34
|
spec.add_dependency("openssl", ">= 3.2.0")
|
|
34
35
|
|
|
35
36
|
spec.add_development_dependency("bcrypt_pbkdf", "~> 1.0") unless RUBY_PLATFORM == "java"
|
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.rc2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jamis Buck
|
|
@@ -32,8 +32,22 @@ cert_chain:
|
|
|
32
32
|
ufHf4AX2UIkJbh7zCPkiNCqIr7MSWLNFG/9lOlHYsEJM8XujT1ofPobYx6YSFx/C
|
|
33
33
|
7HBrI7UX7awt6UvBZebhcHzyMHxg/B5PVQllPA==
|
|
34
34
|
-----END CERTIFICATE-----
|
|
35
|
-
date: 2026-
|
|
35
|
+
date: 2026-10-05 00:00:00.000000000 Z
|
|
36
36
|
dependencies:
|
|
37
|
+
- !ruby/object:Gem::Dependency
|
|
38
|
+
name: logger
|
|
39
|
+
requirement: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
type: :runtime
|
|
45
|
+
prerelease: false
|
|
46
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '0'
|
|
37
51
|
- !ruby/object:Gem::Dependency
|
|
38
52
|
name: openssl
|
|
39
53
|
requirement: !ruby/object:Gem::Requirement
|
metadata.gz.sig
CHANGED
|
Binary file
|