net-ssh 7.2.3 → 7.3.8

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.dockerignore +3 -0
  4. data/.github/workflows/ci-with-docker.yml +21 -7
  5. data/.github/workflows/ci.yml +12 -7
  6. data/.rubocop_todo.yml +3 -1
  7. data/CHANGES.txt +47 -0
  8. data/Dockerfile +3 -0
  9. data/Dockerfile.zlib_ng +29 -0
  10. data/README.md +3 -0
  11. data/Rakefile +20 -12
  12. data/docker-compose.yml +0 -2
  13. data/lib/net/ssh/authentication/ed25519.rb +10 -0
  14. data/lib/net/ssh/authentication/ed25519_loader.rb +2 -2
  15. data/lib/net/ssh/authentication/key_manager.rb +21 -6
  16. data/lib/net/ssh/config.rb +2 -1
  17. data/lib/net/ssh/key_factory.rb +4 -3
  18. data/lib/net/ssh/known_hosts.rb +44 -0
  19. data/lib/net/ssh/proxy/command.rb +28 -3
  20. data/lib/net/ssh/proxy/jump.rb +38 -7
  21. data/lib/net/ssh/service/forward.rb +9 -3
  22. data/lib/net/ssh/test/extensions.rb +3 -2
  23. data/lib/net/ssh/test/socket.rb +1 -1
  24. data/lib/net/ssh/transport/aes128_gcm.rb +40 -0
  25. data/lib/net/ssh/transport/aes256_gcm.rb +40 -0
  26. data/lib/net/ssh/transport/algorithms.rb +8 -1
  27. data/lib/net/ssh/transport/cipher_factory.rb +9 -7
  28. data/lib/net/ssh/transport/gcm_cipher.rb +207 -0
  29. data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
  30. data/lib/net/ssh/transport/packet_stream.rb +7 -3
  31. data/lib/net/ssh/transport/server_version.rb +33 -13
  32. data/lib/net/ssh/transport/session.rb +21 -3
  33. data/lib/net/ssh/transport/state.rb +1 -1
  34. data/lib/net/ssh/verifiers/always.rb +46 -8
  35. data/lib/net/ssh/version.rb +2 -2
  36. data/lib/net/ssh.rb +1 -5
  37. data/net-ssh-public_cert.pem +7 -7
  38. data/net-ssh.gemspec +3 -0
  39. data.tar.gz.sig +0 -0
  40. metadata +44 -12
  41. metadata.gz.sig +0 -0
@@ -1,3 +1,4 @@
1
+ require 'shellwords'
1
2
  require 'uri'
2
3
  require 'net/ssh/proxy/command'
3
4
 
@@ -34,20 +35,50 @@ 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
+ raise ArgumentError, "no jump host given" if jump_proxies.empty?
39
+
38
40
  config = connection_options && connection_options[:config]
39
- uri = URI.parse("ssh://#{first_jump}")
41
+ uri, *extra_jumps = jump_proxies.split(",", -1).map { |jump| validate_jump!(jump) }
40
42
 
41
43
  template = "ssh".dup
42
- template << " -l #{uri.user}" if uri.user
43
- template << " -p #{uri.port}" if uri.port
44
- template << " -J #{extra_jumps}" if extra_jumps
45
- template << " -F #{config}" if config != true && config
44
+ template << " -l #{escape(uri.user)}" if uri.user
45
+ template << " -p #{uri.port}" if uri.port
46
+ template << " -J #{escape(extra_jumps.map { |jump| jump_spec(jump) }.join(','))}" unless extra_jumps.empty?
47
+ template << " -F #{Shellwords.escape(config)}" if config != true && config
46
48
  template << " -W %h:%p "
47
- template << uri.host
49
+ template << escape(uri.host)
48
50
 
49
51
  @command_line_template = template
50
52
  end
53
+
54
+ private
55
+
56
+ # Escape for the shell, and double any % so it survives the %h/%p
57
+ # substitution done on the template in Command#open.
58
+ def escape(value)
59
+ Shellwords.escape(value).gsub("%", "%%")
60
+ end
61
+
62
+ # Only [user@]host[:port] is checked, so anything else URI accepts (a path,
63
+ # query, fragment or password) must be refused before it reaches the shell.
64
+ def validate_jump!(jump)
65
+ uri = URI.parse("ssh://#{jump}")
66
+ raise URI::InvalidURIError if uri.host.to_s.empty? || jump_spec(uri) != jump
67
+
68
+ validate_user_name!(uri.user) if uri.user
69
+ validate_host_name!(uri.host)
70
+ validate_port!(uri.port) if uri.port
71
+ uri
72
+ rescue URI::InvalidURIError
73
+ raise ArgumentError, "jump host is not [user@]host[:port]: #{jump.inspect}"
74
+ end
75
+
76
+ def jump_spec(uri)
77
+ spec = uri.host.dup
78
+ spec.prepend("#{uri.user}@") if uri.user
79
+ spec << ":#{uri.port}" if uri.port
80
+ spec
81
+ end
51
82
  end
52
83
  end
53
84
  end
@@ -48,9 +48,9 @@ module Net
48
48
  # If three arguments are given, it is as if the local bind address is
49
49
  # "127.0.0.1", and the rest are applied as above.
50
50
  #
51
- # To request an ephemeral port on the remote server, provide 0 (zero) for
52
- # the port number. In all cases, this method will return the port that
53
- # has been assigned.
51
+ # To request an ephemeral port on the local server, provide 0 (zero) for
52
+ # the local port number. In all cases, this method will return the port
53
+ # that has been assigned.
54
54
  #
55
55
  # ssh.forward.local(1234, "www.capify.org", 80)
56
56
  # assigned_port = ssh.forward.local("0.0.0.0", 0, "www.capify.org", 80)
@@ -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
 
@@ -57,8 +57,9 @@ module Net
57
57
  return false unless script.next(:first)
58
58
 
59
59
  if script.next(:first).remote?
60
- self.string << script.next.to_s
61
- self.pos = pos
60
+ current_pos = pos
61
+ self.string = "#{self.string}#{script.next}"
62
+ self.pos = current_pos
62
63
  end
63
64
 
64
65
  return true
@@ -22,7 +22,7 @@ module Net
22
22
  # connection.
23
23
  def initialize
24
24
  extend(Net::SSH::Transport::PacketStream)
25
- super "SSH-2.0-Test\r\n"
25
+ super(+"SSH-2.0-Test\r\n")
26
26
 
27
27
  @script = Script.new
28
28
 
@@ -0,0 +1,40 @@
1
+ require 'net/ssh/transport/hmac/abstract'
2
+ require 'net/ssh/transport/gcm_cipher'
3
+
4
+ module Net::SSH::Transport
5
+ ## Implements the aes128-gcm@openssh cipher
6
+ class AES128_GCM
7
+ extend ::Net::SSH::Transport::GCMCipher
8
+
9
+ ## Implicit HMAC, do need to do anything
10
+ class ImplicitHMac < ::Net::SSH::Transport::HMAC::Abstract
11
+ def aead
12
+ true
13
+ end
14
+
15
+ def key_length
16
+ 16
17
+ end
18
+ end
19
+
20
+ def implicit_mac
21
+ ImplicitHMac.new
22
+ end
23
+
24
+ def algo_name
25
+ 'aes-128-gcm'
26
+ end
27
+
28
+ def name
29
+ 'aes128-gcm@openssh.com'
30
+ end
31
+
32
+ #
33
+ # --- RFC 5647 ---
34
+ # K_LEN AES key length 16 octets
35
+ #
36
+ def self.key_length
37
+ 16
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ require 'net/ssh/transport/hmac/abstract'
2
+ require 'net/ssh/transport/gcm_cipher'
3
+
4
+ module Net::SSH::Transport
5
+ ## Implements the aes256-gcm@openssh cipher
6
+ class AES256_GCM
7
+ extend ::Net::SSH::Transport::GCMCipher
8
+
9
+ ## Implicit HMAC, do need to do anything
10
+ class ImplicitHMac < ::Net::SSH::Transport::HMAC::Abstract
11
+ def aead
12
+ true
13
+ end
14
+
15
+ def key_length
16
+ 32
17
+ end
18
+ end
19
+
20
+ def implicit_mac
21
+ ImplicitHMac.new
22
+ end
23
+
24
+ def algo_name
25
+ 'aes-256-gcm'
26
+ end
27
+
28
+ def name
29
+ 'aes256-gcm@openssh.com'
30
+ end
31
+
32
+ #
33
+ # --- RFC 5647 ---
34
+ # K_LEN AES key length 32 octets
35
+ #
36
+ def self.key_length
37
+ 32
38
+ end
39
+ end
40
+ end
@@ -44,7 +44,11 @@ module Net
44
44
  diffie-hellman-group14-sha256
45
45
  diffie-hellman-group14-sha1],
46
46
 
47
- encryption: %w[aes256-ctr aes192-ctr aes128-ctr],
47
+ encryption: %w[aes256-ctr
48
+ aes192-ctr
49
+ aes128-ctr
50
+ aes256-gcm@openssh.com
51
+ aes128-gcm@openssh.com],
48
52
 
49
53
  hmac: %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-etm@openssh.com
50
54
  hmac-sha2-512 hmac-sha2-256
@@ -492,6 +496,9 @@ module Net
492
496
  HMAC.get(hmac_server, mac_key_server, parameters)
493
497
  end
494
498
 
499
+ cipher_client.nonce = iv_client if mac_client.respond_to?(:aead) && mac_client.aead
500
+ cipher_server.nonce = iv_server if mac_server.respond_to?(:aead) && mac_client.aead
501
+
495
502
  session.configure_client cipher: cipher_client, hmac: mac_client,
496
503
  compression: normalize_compression_name(compression_client),
497
504
  compression_level: options[:compression_level],
@@ -1,5 +1,7 @@
1
1
  require 'openssl'
2
2
  require 'net/ssh/transport/ctr.rb'
3
+ require 'net/ssh/transport/aes128_gcm'
4
+ require 'net/ssh/transport/aes256_gcm'
3
5
  require 'net/ssh/transport/key_expander'
4
6
  require 'net/ssh/transport/identity_cipher'
5
7
  require 'net/ssh/transport/chacha20_poly1305_cipher_loader'
@@ -31,15 +33,15 @@ module Net
31
33
  'none' => 'none'
32
34
  }
33
35
 
34
- SSH_TO_CLASS =
36
+ SSH_TO_CLASS = {
37
+ 'aes256-gcm@openssh.com' => Net::SSH::Transport::AES256_GCM,
38
+ 'aes128-gcm@openssh.com' => Net::SSH::Transport::AES128_GCM
39
+ }.tap do |hash|
35
40
  if Net::SSH::Transport::ChaCha20Poly1305CipherLoader::LOADED
36
- {
37
- 'chacha20-poly1305@openssh.com' => Net::SSH::Transport::ChaCha20Poly1305Cipher
38
- }
39
- else
40
- {
41
- }
41
+ hash['chacha20-poly1305@openssh.com'] =
42
+ Net::SSH::Transport::ChaCha20Poly1305Cipher
42
43
  end
44
+ end
43
45
 
44
46
  # Returns true if the underlying OpenSSL library supports the given cipher,
45
47
  # and false otherwise.
@@ -0,0 +1,207 @@
1
+ require 'net/ssh/loggable'
2
+
3
+ module Net
4
+ module SSH
5
+ module Transport
6
+ ## Extension module for aes(128|256)gcm ciphers
7
+ module GCMCipher
8
+ # rubocop:disable Metrics/AbcSize
9
+ def self.extended(orig)
10
+ # rubocop:disable Metrics/BlockLength
11
+ orig.class_eval do
12
+ include Net::SSH::Loggable
13
+
14
+ attr_reader :cipher
15
+ attr_reader :key
16
+ attr_reader :nonce
17
+
18
+ #
19
+ # Semantically gcm cipher supplies the OpenSSL iv interface with a nonce
20
+ # as it is not randomly generated due to being supplied from a counter.
21
+ # The RFC's use IV and nonce interchangeably.
22
+ #
23
+ def initialize(encrypt:, key:)
24
+ @cipher = OpenSSL::Cipher.new(algo_name)
25
+ @key = key
26
+ key_len = @cipher.key_len
27
+ if key.size != key_len
28
+ error_message = "#{cipher_name}: keylength does not match"
29
+ error { error_message }
30
+ raise error_message
31
+ end
32
+ encrypt ? @cipher.encrypt : @cipher.decrypt
33
+ @cipher.key = key
34
+
35
+ @nonce = {
36
+ fixed: nil,
37
+ invocation_counter: 0
38
+ }
39
+ end
40
+
41
+ def update_cipher_mac(payload, _sequence_number)
42
+ #
43
+ # --- RFC 5647 7.3 ---
44
+ # When using AES-GCM with secure shell, the packet_length field is to
45
+ # be treated as additional authenticated data, not as plaintext.
46
+ #
47
+ length_data = [payload.bytesize].pack('N')
48
+
49
+ cipher.auth_data = length_data
50
+
51
+ encrypted_data = cipher.update(payload) << cipher.final
52
+
53
+ mac = cipher.auth_tag
54
+
55
+ incr_nonce
56
+ length_data + encrypted_data + mac
57
+ end
58
+
59
+ #
60
+ # --- RFC 5647 ---
61
+ # uint32 packet_length; // 0 <= packet_length < 2^32
62
+ #
63
+ def read_length(data, _sequence_number)
64
+ data.unpack1('N')
65
+ end
66
+
67
+ #
68
+ # --- RFC 5647 ---
69
+ # In AES-GCM secure shell, the inputs to the authenticated encryption
70
+ # are:
71
+ # PT (Plain Text)
72
+ # byte padding_length; // 4 <= padding_length < 256
73
+ # byte[n1] payload; // n1 = packet_length-padding_length-1
74
+ # byte[n2] random_padding; // n2 = padding_length
75
+ # AAD (Additional Authenticated Data)
76
+ # uint32 packet_length; // 0 <= packet_length < 2^32
77
+ # IV (Initialization Vector)
78
+ # As described in section 7.1.
79
+ # BK (Block Cipher Key)
80
+ # The appropriate Encryption Key formed during the Key Exchange.
81
+ #
82
+ def read_and_mac(data, mac, _sequence_number)
83
+ # The authentication tag will be placed in the MAC field at the end of the packet
84
+
85
+ # OpenSSL does not verify auth tag length
86
+ # GCM mode allows arbitrary sizes for the auth_tag up to 128 bytes and a single
87
+ # byte allows authentication to pass. If single byte auth tags are possible
88
+ # an attacker would require no more than 256 attempts to forge a valid tag.
89
+ #
90
+ raise 'incorrect auth_tag length' unless mac.to_s.length == mac_length
91
+
92
+ packet_length = data.unpack1('N')
93
+
94
+ cipher.auth_tag = mac.to_s
95
+ cipher.auth_data = [packet_length].pack('N')
96
+
97
+ result = cipher.update(data[4...]) << cipher.final
98
+ incr_nonce
99
+ result
100
+ end
101
+
102
+ def mac_length
103
+ 16
104
+ end
105
+
106
+ def block_size
107
+ 16
108
+ end
109
+
110
+ def self.block_size
111
+ 16
112
+ end
113
+
114
+ #
115
+ # --- RFC 5647 ---
116
+ # N_MIN minimum nonce (IV) length 12 octets
117
+ # N_MAX maximum nonce (IV) length 12 octets
118
+ #
119
+ def iv_len
120
+ 12
121
+ end
122
+
123
+ #
124
+ # --- RFC 5288 ---
125
+ # Each value of the nonce_explicit MUST be distinct for each distinct
126
+ # invocation of the GCM encrypt function for any fixed key. Failure to
127
+ # meet this uniqueness requirement can significantly degrade security.
128
+ # The nonce_explicit MAY be the 64-bit sequence number.
129
+ #
130
+ # --- RFC 5116 ---
131
+ # (2.1) Applications that can generate distinct nonces SHOULD use the nonce
132
+ # formation method defined in Section 3.2, and MAY use any
133
+ # other method that meets the uniqueness requirement.
134
+ #
135
+ # (3.2) The following method to construct nonces is RECOMMENDED.
136
+ #
137
+ # <- variable -> <- variable ->
138
+ # - - - - - - - - - - - - - -
139
+ # | fixed | counter |
140
+ #
141
+ # Initial octets consist of a fixed field and final octets consist of a
142
+ # Counter field. Implementations SHOULD support 12-octet nonces in which
143
+ # the Counter field is four octets long.
144
+ # The Counter fields of successive nonces form a monotonically increasing
145
+ # sequence, when those fields are regarded as unsignd integers in network
146
+ # byte order.
147
+ # The Counter part SHOULD be equal to zero for the first nonce and increment
148
+ # by one for each successive nonce that is generated.
149
+ # The Fixed field MUST remain constant for all nonces that are generated for
150
+ # a given encryption device.
151
+ #
152
+ # --- RFC 5647 ---
153
+ # The invocation field is treated as a 64-bit integer and is increment after
154
+ # each invocation of AES-GCM to process a binary packet.
155
+ # AES-GCM produces a keystream in blocks of 16-octets that is used to
156
+ # encrypt the plaintext. This keystream is produced by encrypting the
157
+ # following 16-octet data structure:
158
+ #
159
+ # uint32 fixed; // 4 octets
160
+ # uint64 invocation_counter; // 8 octets
161
+ # unit32 block_counter; // 4 octets
162
+ #
163
+ # The block_counter is initially set to one (1) and increment as each block
164
+ # of key is produced.
165
+ #
166
+ # The reader is reminded that SSH requires that the data to be encrypted
167
+ # MUST be padded out to a multiple of the block size (16-octets for AES-GCM).
168
+ #
169
+ def incr_nonce
170
+ return if nonce[:fixed].nil?
171
+
172
+ nonce[:invocation_counter] = [nonce[:invocation_counter].to_s.unpack1('B*').to_i(2) + 1].pack('Q>*')
173
+
174
+ apply_nonce
175
+ end
176
+
177
+ def nonce=(iv_s)
178
+ return if nonce[:fixed]
179
+
180
+ nonce[:fixed] = iv_s[0...4]
181
+ nonce[:invocation_counter] = iv_s[4...12]
182
+
183
+ apply_nonce
184
+ end
185
+
186
+ def apply_nonce
187
+ cipher.iv = "#{nonce[:fixed]}#{nonce[:invocation_counter]}"
188
+ end
189
+
190
+ #
191
+ # --- RFC 5647 ---
192
+ # If AES-GCM is selected as the encryption algorithm for a given
193
+ # tunnel, AES-GCM MUST also be selected as the Message Authentication
194
+ # Code (MAC) algorithm. Conversely, if AES-GCM is selected as the MAC
195
+ # algorithm, it MUST also be selected as the encryption algorithm.
196
+ #
197
+ def implicit_mac?
198
+ true
199
+ end
200
+ end
201
+ end
202
+ # rubocop:enable Metrics/BlockLength
203
+ end
204
+ # rubocop:enable Metrics/AbcSize
205
+ end
206
+ end
207
+ end
@@ -8,6 +8,18 @@ module Net
8
8
  # The base class of all OpenSSL-based HMAC algorithm wrappers.
9
9
  class Abstract
10
10
  class << self
11
+ def aead(*v)
12
+ @aead = false if !defined?(@aead)
13
+ if v.empty?
14
+ @aead = superclass.aead if @aead.nil? && superclass.respond_to?(:aead)
15
+ return @aead
16
+ elsif v.length == 1
17
+ @aead = v.first
18
+ else
19
+ raise ArgumentError, "wrong number of arguments (#{v.length} for 1)"
20
+ end
21
+ end
22
+
11
23
  def etm(*v)
12
24
  @etm = false if !defined?(@etm)
13
25
  if v.empty?
@@ -57,6 +69,10 @@ module Net
57
69
  end
58
70
  end
59
71
 
72
+ def aead
73
+ self.class.aead
74
+ end
75
+
60
76
  def etm
61
77
  self.class.etm
62
78
  end
@@ -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)
@@ -128,7 +131,7 @@ module Net
128
131
  payload = client.compress(payload)
129
132
 
130
133
  # the length of the packet, minus the padding
131
- actual_length = (client.hmac.etm ? 0 : 4) + payload.bytesize + 1
134
+ actual_length = (client.hmac.etm || client.hmac.aead ? 0 : 4) + payload.bytesize + 1
132
135
 
133
136
  # compute the padding length
134
137
  padding_length = client.block_size - (actual_length % client.block_size)
@@ -151,7 +154,7 @@ module Net
151
154
  debug { "using encrypt-then-mac" }
152
155
 
153
156
  # Encrypt padding_length, payload, and padding. Take MAC
154
- # from the unencrypted packet_lenght and the encrypted
157
+ # from the unencrypted packet_length and the encrypted
155
158
  # data.
156
159
  length_data = [packet_length].pack("N")
157
160
 
@@ -219,7 +222,7 @@ module Net
219
222
  # new Packet object.
220
223
  # rubocop:disable Metrics/AbcSize
221
224
  def poll_next_packet
222
- aad_length = server.hmac.etm ? 4 : 0
225
+ aad_length = server.hmac.etm || server.hmac.aead ? 4 : 0
223
226
 
224
227
  if @packet.nil?
225
228
  minimum = server.block_size < 4 ? 4 : server.block_size
@@ -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
- 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
 
@@ -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
 
@@ -102,8 +107,8 @@ module Net
102
107
  # SSH known-host files.
103
108
  def host_as_string
104
109
  @host_as_string ||= begin
105
- string = "#{host}"
106
- string = "[#{string}]:#{port}" if port != DEFAULT_PORT
110
+ string = +"#{host}"
111
+ string = +"[#{string}]:#{port}" if port != DEFAULT_PORT
107
112
 
108
113
  peer_ip = socket.peer_ip
109
114
 
@@ -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 @queue.shift if consume_queue && @queue.any? && algorithms.allow?(@queue.first)
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,8 @@ module Net
211
216
 
212
217
  else
213
218
  return packet if algorithms.allow?(packet)
219
+ # Algorithms#rekey! clears initialized?, so the session id is what marks the first key exchange as done.
220
+ raise Net::SSH::Exception, "unexpected packet type #{packet.type} during initial key exchange" unless algorithms.session_id
214
221
 
215
222
  push(packet)
216
223
  end
@@ -234,6 +241,11 @@ module Net
234
241
  # #poll_message will return packets from the queue in the order they
235
242
  # were received.
236
243
  def push(packet)
244
+ @queued_bytes += packet.length
245
+ if @queue.size >= MAX_QUEUED_PACKETS || @queued_bytes > MAX_QUEUED_BYTES
246
+ raise Net::SSH::Exception, "too many packets queued during key exchange"
247
+ end
248
+
237
249
  @queue.push(packet)
238
250
  end
239
251
 
@@ -276,6 +288,12 @@ module Net
276
288
 
277
289
  private
278
290
 
291
+ def shift_queue
292
+ packet = @queue.shift
293
+ @queued_bytes -= packet.length
294
+ packet
295
+ end
296
+
279
297
  # Compatibility verifier which allows users to keep using
280
298
  # custom verifier code without adding new :verify_signature
281
299
  # method.
@@ -125,7 +125,7 @@ module Net
125
125
  compressor.deflate(data, Zlib::SYNC_FLUSH)
126
126
  end
127
127
 
128
- # Deompresses the data. If no compression is in effect, this will just return
128
+ # Decompresses the data. If no compression is in effect, this will just return
129
129
  # the data unmodified, otherwise it uses #decompressor to decompress the data.
130
130
  def decompress(data)
131
131
  data = data.to_s