net-ssh 7.3.7 → 7.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20d1aa171eb7f2135e994d7d7c495400ad619159a0d67a287336466512f43dec
4
- data.tar.gz: acaf01c452aab18fc52d7994295d6ca4c843065d0d1044d1fa86e0f9ba542db5
3
+ metadata.gz: b3711427d13347329810ecb931a8b67d7a4166364df280fd3c9bb98f80e6b79c
4
+ data.tar.gz: 367a9ff71b98dfc29aeed8b0fe11156b469d25d51c835e93c25366d0860df5c6
5
5
  SHA512:
6
- metadata.gz: 570ef6dcb8286b230c8e97b41bd6efa2d0358effee0b55e23de2d6392785d71dd220f0d39bd90e232a7a84ebfe97ddbcb0a9bfa6263e3c180a5a3fed894ceac8
7
- data.tar.gz: f7c2f6c2eec63eab80e632d459b8f4c424cefea276077ad484fa1b0eb94489a86fc6b620e682100e4f5d9dba89df39e3f50384019d136ef6cbf0bf52f6fbb800
6
+ metadata.gz: 3adc06b15596a5d708cfd206ec5efb91461de3458042da6e3eff25496e0f249cd4070197e393ce674b1dc40feb4862e643da4c70790af2be76321f74744974ea
7
+ data.tar.gz: 9d419e3dfadede74db2b04a897185aa36bcc3e84c52479e9ae0be06275fb497c9080cd209419171205d5a5f8c5b7beed001cd94322c8427c35759de67433bed0
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGES.txt CHANGED
@@ -1,3 +1,11 @@
1
+ === 7.3.9
2
+
3
+ * Security: refuse host keys marked `@revoked` in known_hosts, and certificates of such a key or signed by it, by raising the new `Net::SSH::HostKeyRevoked`, which `remember_host!` can't override; these lines were loaded as trusted keys, so every verifier except `:never` accepted the revoked key [GHSA-hjwv-j52r-xcg7]
4
+
5
+ === 7.3.8
6
+
7
+ * Security: validate every ProxyJump hop, not only the first, as [user@]host[:port] (refusing a path, query, fragment or password) and shell-escape the hops passed to ssh -J; a hop such as `evil/x&$(cmd)` ran `cmd` through the shell [GHSA-g2r2-x3p2-5677]
8
+
1
9
  === 7.3.7
2
10
 
3
11
  * Fixed `unexpected packet type 94 during initial key exchange` (or 93) when channel data arrives during a client-initiated re-key, which broke bulk transfers at the first re-key (e.g. with `rekey_limit`, or after about 1 GB); a regression in 7.3.5 [#1063]
@@ -102,5 +102,15 @@ module Net
102
102
  # you want to proceed anyway, simply call the remember_host! method on the
103
103
  # exception, and then retry.
104
104
  class HostKeyUnknown < HostKeyError; end
105
+
106
+ # Raised when the key given by the host, or the CA that signed its
107
+ # certificate, is marked @revoked in a known hosts file. A revoked key must
108
+ # never be accepted, so unlike the other host key errors this one can't be
109
+ # overridden: #remember_host! just raises it again.
110
+ class HostKeyRevoked < HostKeyError
111
+ def remember_host!
112
+ raise self
113
+ end
114
+ end
105
115
  end
106
116
  end
@@ -93,6 +93,22 @@ module Net
93
93
  server_key.critical_options.empty?
94
94
  end
95
95
  end
96
+
97
+ # @revoked entry
98
+ class Revoked
99
+ def initialize(key, comment: nil)
100
+ @key = key
101
+ @comment = comment
102
+ end
103
+
104
+ # Returns true for the revoked key itself and for any certificate that
105
+ # wraps it or is signed by it, matching OpenSSH's check_key_not_revoked.
106
+ def matches_key?(server_key)
107
+ keys = [server_key]
108
+ keys += [server_key.key, server_key.signature_key] if server_key.respond_to?(:signature_key)
109
+ keys.any? { |key| key.ssh_type == @key.ssh_type && key.to_blob == @key.to_blob }
110
+ end
111
+ end
96
112
  end
97
113
 
98
114
  # Represents the result of a search in known hosts
@@ -101,11 +117,17 @@ module Net
101
117
  include Enumerable
102
118
  attr_reader :host
103
119
 
104
- def initialize(host_keys, host, known_hosts, options = {})
120
+ def initialize(host_keys, host, known_hosts, options = {}, revoked_keys = [])
105
121
  @host_keys = host_keys
106
122
  @host = host
107
123
  @known_hosts = known_hosts
108
124
  @options = options
125
+ @revoked_keys = revoked_keys
126
+ end
127
+
128
+ # Returns true if a @revoked known-hosts entry for this host matches +key+.
129
+ def revoked?(key)
130
+ @revoked_keys.any? { |entry| entry.matches_key?(key) }
109
131
  end
110
132
 
111
133
  def add_host_key(key)
@@ -151,11 +173,14 @@ module Net
151
173
  # Searches all known host files (see KnownHosts.hostfiles) for all keys
152
174
  # of the given host. Returns an enumerable of keys found.
153
175
  def search_for(host, options = {})
154
- HostKeys.new(search_in(hostfiles(options), host, options), host, self, options)
176
+ files = hostfiles(options)
177
+ revoked_keys = files.flat_map { |file| KnownHosts.new(file).revoked_keys_for(host) }
178
+ HostKeys.new(search_in(files, host, options), host, self, options, revoked_keys)
155
179
  end
156
180
 
157
181
  # Search for all known keys for the given host, in every file given in
158
- # the +files+ array. Returns the list of keys.
182
+ # the +files+ array. Returns the list of keys. @revoked entries are not
183
+ # included.
159
184
  def search_in(files, host, options = {})
160
185
  files.flat_map { |file| KnownHosts.new(file).keys_for(host, options) }
161
186
  end
@@ -219,46 +244,50 @@ module Net
219
244
  # "[net.ssh.test]:5555"
220
245
  # "[1,2,3,4]:5555"
221
246
  # "[net.ssh.test]:5555,[1.2.3.4]:5555
247
+ #
248
+ # @revoked entries are not included, see revoked_keys_for.
222
249
  def keys_for(host, options = {})
223
250
  keys = []
224
- return keys unless File.readable?(source)
225
-
226
251
  entries = host.split(/,/)
227
252
  host_name = entries[0]
228
253
  host_ip = entries[1]
229
254
 
230
- File.open(source) do |file|
231
- file.each_line do |line|
232
- if line.start_with?('@')
233
- marker, hosts, type, key_content, comment = line.split(' ')
234
- else
235
- marker = nil
236
- hosts, type, key_content, comment = line.split(' ')
237
- end
255
+ each_entry do |marker, hostlist, key_content, comment|
256
+ next if marker == "@revoked"
238
257
 
239
- # Skip empty line or one that is commented
240
- next if hosts.nil? || hosts.start_with?('#')
258
+ found = hostlist.any? { |pattern| match(host_name, pattern) } || known_host_hash?(hostlist, entries)
259
+ next unless found
241
260
 
242
- hostlist = hosts.split(',')
261
+ found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1
262
+ next unless found
243
263
 
244
- next unless SUPPORTED_TYPE.include?(type)
264
+ raw_key = read_key(key_content)
265
+
266
+ keys <<
267
+ if marker == "@cert-authority"
268
+ HostKeyEntries::CertAuthority.new(raw_key, comment: comment)
269
+ else
270
+ HostKeyEntries::PubKey.new(raw_key, comment: comment)
271
+ end
272
+ end
245
273
 
246
- found = hostlist.any? { |pattern| match(host_name, pattern) } || known_host_hash?(hostlist, entries)
247
- next unless found
274
+ keys
275
+ end
248
276
 
249
- found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1
250
- next unless found
277
+ # Returns the @revoked entries for the given host, which takes the same
278
+ # formats as keys_for. An entry matching any of the comma-separated names
279
+ # counts, so :check_host_ip can't narrow a revocation.
280
+ def revoked_keys_for(host)
281
+ keys = []
282
+ entries = host.split(/,/)
251
283
 
252
- blob = key_content.unpack("m*").first
253
- raw_key = Net::SSH::Buffer.new(blob).read_key
284
+ each_entry do |marker, hostlist, key_content, comment|
285
+ next unless marker == "@revoked"
254
286
 
255
- keys <<
256
- if marker == "@cert-authority"
257
- HostKeyEntries::CertAuthority.new(raw_key, comment: comment)
258
- else
259
- HostKeyEntries::PubKey.new(raw_key, comment: comment)
260
- end
261
- end
287
+ found = entries.any? { |entry| hostlist.any? { |pattern| match(entry, pattern) } }
288
+ next unless found || known_host_hash?(hostlist, entries)
289
+
290
+ keys << HostKeyEntries::Revoked.new(read_key(key_content), comment: comment)
262
291
  end
263
292
 
264
293
  keys
@@ -303,6 +332,32 @@ module Net
303
332
  file.puts "#{host} #{key.ssh_type} #{blob}"
304
333
  end
305
334
  end
335
+
336
+ private
337
+
338
+ # Yields the marker, host patterns, key and comment of each usable line.
339
+ def each_entry
340
+ return unless File.readable?(source)
341
+
342
+ File.open(source) do |file|
343
+ file.each_line do |line|
344
+ fields = line.split(' ')
345
+ marker = fields.shift if fields.first&.start_with?('@')
346
+ hosts, type, key_content, comment = fields
347
+
348
+ # Skip empty line or one that is commented
349
+ next if hosts.nil? || hosts.start_with?('#')
350
+
351
+ next unless SUPPORTED_TYPE.include?(type)
352
+
353
+ yield marker, hosts.split(','), key_content, comment
354
+ end
355
+ end
356
+ end
357
+
358
+ def read_key(key_content)
359
+ Net::SSH::Buffer.new(key_content.unpack("m*").first).read_key
360
+ end
306
361
  end
307
362
  end
308
363
  end
@@ -35,28 +35,49 @@ module Net
35
35
  # We cannot build the ProxyCommand template until we know if the :config
36
36
  # option was specified during `Net::SSH.start`.
37
37
  def build_proxy_command_equivalent(connection_options = nil)
38
- extra_jumps = jump_proxies.split(",", 2)[1]
38
+ raise ArgumentError, "no jump host given" if jump_proxies.empty?
39
+
39
40
  config = connection_options && connection_options[:config]
40
- uri, = jump_proxies.split(",").map { |jump| validate_jump!(jump) }
41
+ uri, *extra_jumps = jump_proxies.split(",", -1).map { |jump| validate_jump!(jump) }
41
42
 
42
43
  template = "ssh".dup
43
- template << " -l #{uri.user}" if uri.user
44
- template << " -p #{uri.port}" if uri.port
45
- template << " -J #{extra_jumps}" if extra_jumps
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?
46
47
  template << " -F #{Shellwords.escape(config)}" if config != true && config
47
48
  template << " -W %h:%p "
48
- template << uri.host
49
+ template << escape(uri.host)
49
50
 
50
51
  @command_line_template = template
51
52
  end
52
53
 
53
54
  private
54
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.
55
64
  def validate_jump!(jump)
56
65
  uri = URI.parse("ssh://#{jump}")
66
+ raise URI::InvalidURIError if uri.host.to_s.empty? || jump_spec(uri) != jump
67
+
57
68
  validate_user_name!(uri.user) if uri.user
58
69
  validate_host_name!(uri.host)
70
+ validate_port!(uri.port) if uri.port
59
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
60
81
  end
61
82
  end
62
83
  end
@@ -7,11 +7,13 @@ module Net
7
7
  # verify a connection if the server is the localhost and the port is a
8
8
  # nonstandard port number. Those two conditions will typically mean the
9
9
  # connection is being tunnelled through a forwarded port, so the known-hosts
10
- # file will not be helpful (in general).
10
+ # file will not be helpful (in general). A key marked @revoked is refused
11
+ # either way.
11
12
  class AcceptNewOrLocalTunnel < AcceptNew
12
13
  # Tries to determine if the connection is being tunnelled, and if so,
13
14
  # returns true. Otherwise, performs the standard strict verification.
14
15
  def verify(arguments)
16
+ refuse_revoked!(arguments)
15
17
  return true if tunnelled?(arguments)
16
18
 
17
19
  super
@@ -10,10 +10,13 @@ module Net
10
10
  # (HostKeyUnknown). This is in contrast to the "Strict" class, which will
11
11
  # silently add the key to your known_hosts file. If the server does appear at
12
12
  # least once, but the key given does not match any known for the server, an
13
- # exception will be raised (HostKeyMismatch).
13
+ # exception will be raised (HostKeyMismatch). A key marked @revoked in a
14
+ # host file is always refused (HostKeyRevoked).
14
15
  # Otherwise, this returns true.
15
16
  class Always
16
17
  def verify(arguments)
18
+ refuse_revoked!(arguments)
19
+
17
20
  host_keys = arguments[:session].host_keys
18
21
 
19
22
  # We've never seen this host before, so raise an exception.
@@ -47,6 +50,18 @@ module Net
47
50
 
48
51
  private
49
52
 
53
+ # Custom :known_hosts objects may return a host_keys collection with no
54
+ # revocation support.
55
+ def refuse_revoked!(arguments)
56
+ host_keys = arguments[:session].host_keys
57
+ return unless host_keys.respond_to?(:revoked?) && host_keys.revoked?(arguments[:key])
58
+
59
+ exception = HostKeyRevoked.new("fingerprint #{arguments[:fingerprint]} " +
60
+ "is revoked for #{host_keys.host.inspect}")
61
+ exception.data = arguments
62
+ raise exception
63
+ end
64
+
50
65
  # Enforces the @cert-authority certificate constraints for entries that
51
66
  # implement them (Net::SSH::HostKeyEntries::CertAuthority). Non-certificate
52
67
  # entries do not respond to these predicates and are left untouched.
@@ -3,7 +3,8 @@ module Net
3
3
  module Verifiers
4
4
  # This host key verifier simply allows every key it sees, without
5
5
  # any verification. This is simple, but very insecure because it
6
- # exposes you to MiTM attacks.
6
+ # exposes you to MiTM attacks. It does not read the known hosts files,
7
+ # so it accepts keys marked @revoked as well.
7
8
  class Never
8
9
  # Returns true.
9
10
  def verify(arguments)
@@ -52,7 +52,7 @@ module Net
52
52
  MINOR = 3
53
53
 
54
54
  # The tiny component of this version of the Net::SSH library
55
- TINY = 7
55
+ TINY = 9
56
56
 
57
57
  # The prerelease component of this version of the Net::SSH library
58
58
  # nil allowed
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.7
4
+ version: 7.3.9
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-25 00:00:00.000000000 Z
35
+ date: 2026-11-06 00:00:00.000000000 Z
36
36
  dependencies:
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: logger
metadata.gz.sig CHANGED
Binary file