net-ssh 7.3.8 → 7.3.10
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 +8 -0
- data/lib/net/ssh/errors.rb +10 -0
- data/lib/net/ssh/known_hosts.rb +85 -30
- data/lib/net/ssh/proxy/https.rb +28 -4
- data/lib/net/ssh/verifiers/accept_new_or_local_tunnel.rb +3 -1
- data/lib/net/ssh/verifiers/always.rb +16 -1
- data/lib/net/ssh/verifiers/never.rb +2 -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: db3535a095da636205644f63d089fa686d964ad5adfda36d126e5ca39824f908
|
|
4
|
+
data.tar.gz: 9786dcb2366e2e7a745bb0635d0fd31d14878c5d994f71c00bfaf3f5c756e74a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bd1d6c6cb705aa0688a84eaa736e8032f9bda0ae5bf8a138d7b9bbd13a284e76440a5a3f7d37572026e4ab24bab2dac030b86ab522f532f54da32b724e619de6
|
|
7
|
+
data.tar.gz: 4c5f42af817e046a29302fc2555c16532c34e47f3dfb0e57a2357b7fae3ed67fe389c87313950fada4cbd6dda3f2857466bc1afd7c2316eaa072bfda1edb79ea
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGES.txt
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
=== 7.3.10
|
|
2
|
+
|
|
3
|
+
* Security: verify the proxy's certificate and hostname in `Net::SSH::Proxy::HTTPS` by default, and the hostname with any verifying `:ssl_context`; any certificate was accepted, exposing the proxy credentials to a man-in-the-middle. A self-signed or private CA proxy now needs an `:ssl_context` that trusts it, or one with `verify_mode = OpenSSL::SSL::VERIFY_NONE` to skip verification. Thanks @fuzzyrichie [GHSA-qcf4-j55x-72hv]
|
|
4
|
+
|
|
5
|
+
=== 7.3.9
|
|
6
|
+
|
|
7
|
+
* 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]
|
|
8
|
+
|
|
1
9
|
=== 7.3.8
|
|
2
10
|
|
|
3
11
|
* 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]
|
data/lib/net/ssh/errors.rb
CHANGED
|
@@ -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
|
data/lib/net/ssh/known_hosts.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
240
|
-
|
|
258
|
+
found = hostlist.any? { |pattern| match(host_name, pattern) } || known_host_hash?(hostlist, entries)
|
|
259
|
+
next unless found
|
|
241
260
|
|
|
242
|
-
|
|
261
|
+
found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1
|
|
262
|
+
next unless found
|
|
243
263
|
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
274
|
+
keys
|
|
275
|
+
end
|
|
248
276
|
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
253
|
-
|
|
284
|
+
each_entry do |marker, hostlist, key_content, comment|
|
|
285
|
+
next unless marker == "@revoked"
|
|
254
286
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
data/lib/net/ssh/proxy/https.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'socket'
|
|
2
2
|
require 'openssl'
|
|
3
|
+
require 'resolv'
|
|
3
4
|
require 'net/ssh/proxy/errors'
|
|
4
5
|
require 'net/ssh/proxy/http'
|
|
5
6
|
|
|
@@ -15,10 +16,14 @@ module Net
|
|
|
15
16
|
# can be used to tweak this proxy connection. In addition to the options
|
|
16
17
|
# taken by Net::SSH::Proxy::HTTP it supports:
|
|
17
18
|
#
|
|
18
|
-
# * :ssl_context => the SSL configuration to use for the connection
|
|
19
|
+
# * :ssl_context => the SSL configuration to use for the connection. The
|
|
20
|
+
# default verifies the proxy's certificate and hostname against the
|
|
21
|
+
# system trust store. For a self-signed or private CA certificate pass a
|
|
22
|
+
# context with it in +cert_store+, or, to skip verification and expose
|
|
23
|
+
# the proxy credentials to a man-in-the-middle, a context with
|
|
24
|
+
# +verify_mode+ set to OpenSSL::SSL::VERIFY_NONE.
|
|
19
25
|
def initialize(proxy_host, proxy_port = 80, options = {})
|
|
20
|
-
@ssl_context = options.delete(:ssl_context) ||
|
|
21
|
-
OpenSSL::SSL::SSLContext.new
|
|
26
|
+
@ssl_context = options.delete(:ssl_context) || default_ssl_context
|
|
22
27
|
super(proxy_host, proxy_port, options)
|
|
23
28
|
end
|
|
24
29
|
|
|
@@ -41,9 +46,28 @@ module Net
|
|
|
41
46
|
plain_socket = super(connect_timeout)
|
|
42
47
|
OpenSSL::SSL::SSLSocket.new(plain_socket, @ssl_context).tap do |socket|
|
|
43
48
|
socket.extend(SSLSocketCompatibility)
|
|
44
|
-
|
|
49
|
+
# SNI must not carry an IP address (RFC 6066).
|
|
50
|
+
socket.hostname = proxy_host unless Resolv::AddressRegex.match?(proxy_host)
|
|
51
|
+
begin
|
|
52
|
+
socket.connect
|
|
53
|
+
socket.post_connection_check(proxy_host) if verify_peer?
|
|
54
|
+
rescue StandardError
|
|
55
|
+
socket.close
|
|
56
|
+
raise
|
|
57
|
+
end
|
|
45
58
|
end
|
|
46
59
|
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def default_ssl_context
|
|
64
|
+
OpenSSL::SSL::SSLContext.new.tap(&:set_params)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# verify_mode is nil on a bare SSLContext in older openssl gems.
|
|
68
|
+
def verify_peer?
|
|
69
|
+
@ssl_context.verify_mode.to_i != OpenSSL::SSL::VERIFY_NONE
|
|
70
|
+
end
|
|
47
71
|
end
|
|
48
72
|
end
|
|
49
73
|
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)
|
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.10
|
|
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-
|
|
35
|
+
date: 2026-11-08 00:00:00.000000000 Z
|
|
36
36
|
dependencies:
|
|
37
37
|
- !ruby/object:Gem::Dependency
|
|
38
38
|
name: logger
|
metadata.gz.sig
CHANGED
|
Binary file
|