summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--paramiko/transport.py16
-rw-r--r--sites/www/changelog.rst6
-rw-r--r--tests/_util.py7
-rw-r--r--tests/test_transport.py58
4 files changed, 76 insertions, 11 deletions
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 30d855d7..10573031 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -2102,6 +2102,20 @@ class Transport(threading.Thread, ClosingContextManager):
# be empty.)
return reply
+ def _enforce_strict_kex(self, ptype):
+ """
+ Conditionally raise `MessageOrderError` during strict initial kex.
+
+ This method should only be called inside code that handles non-KEXINIT
+ messages; it does not interrogate ``ptype`` besides using it to log
+ more accurately.
+ """
+ if self.agreed_on_strict_kex and not self.initial_kex_done:
+ name = MSG_NAMES.get(ptype, f"msg {ptype}")
+ raise MessageOrderError(
+ f"In strict-kex mode, but was sent {name!r}!"
+ )
+
def run(self):
# (use the exposed "run" method, because if we specify a thread target
# of a private method, threading.Thread will keep a reference to it
@@ -2146,11 +2160,13 @@ class Transport(threading.Thread, ClosingContextManager):
except NeedRekeyException:
continue
if ptype == MSG_IGNORE:
+ self._enforce_strict_kex(ptype)
continue
elif ptype == MSG_DISCONNECT:
self._parse_disconnect(m)
break
elif ptype == MSG_DEBUG:
+ self._enforce_strict_kex(ptype)
self._parse_debug(m)
continue
if len(self._expected_packet) > 0:
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 8f745b42..682e3beb 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -24,9 +24,9 @@ Changelog
unless you override this by specifying ``strict_kex=False`` in
`Transport.__init__`.
- Paramiko will now raise an `SSHException` subclass (`MessageOrderError`)
- when protocol messages are received in unexpected order. (This is not
- *really* a change in behavior, as most such cases already raised vanilla
- `SSHException` anyways.)
+ when protocol messages are received in unexpected order. This includes
+ situations like receiving ``MSG_DEBUG`` or ``MSG_IGNORE`` during initial
+ key exchange, which are no longer allowed during strict mode.
- Key (re)negotiation -- i.e. ``MSG_NEWKEYS``, whenever it is encountered
-- now resets packet sequence numbers. (This should be invisible to users
during normal operation, only causing exceptions if the exploit is
diff --git a/tests/_util.py b/tests/_util.py
index f705e934..f0ae1d41 100644
--- a/tests/_util.py
+++ b/tests/_util.py
@@ -346,6 +346,7 @@ def server(
pubkeys=None,
catch_error=False,
transport_factory=None,
+ server_transport_factory=None,
defer=False,
skip_verify=False,
):
@@ -373,6 +374,8 @@ def server(
Necessary for connection_time exception testing.
:param transport_factory:
Like the same-named param in SSHClient: which Transport class to use.
+ :param server_transport_factory:
+ Like ``transport_factory``, but only impacts the server transport.
:param bool defer:
Whether to defer authentication during connecting.
@@ -399,8 +402,10 @@ def server(
sockc.link(socks)
if transport_factory is None:
transport_factory = Transport
+ if server_transport_factory is None:
+ server_transport_factory = transport_factory
tc = transport_factory(sockc, **dict(init, **client_init))
- ts = transport_factory(socks, **dict(init, **server_init))
+ ts = server_transport_factory(socks, **dict(init, **server_init))
if hostkey is None:
hostkey = RSAKey.from_private_key_file(_support("rsa.key"))
diff --git a/tests/test_transport.py b/tests/test_transport.py
index ecf0a184..655ae071 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -52,11 +52,15 @@ from paramiko.common import (
MAX_WINDOW_SIZE,
MIN_PACKET_SIZE,
MIN_WINDOW_SIZE,
+ MSG_CHANNEL_OPEN,
+ MSG_DEBUG,
+ MSG_IGNORE,
MSG_KEXINIT,
+ MSG_UNIMPLEMENTED,
MSG_USERAUTH_SUCCESS,
+ byte_chr,
cMSG_CHANNEL_WINDOW_ADJUST,
cMSG_UNIMPLEMENTED,
- byte_chr,
)
from paramiko.message import Message
@@ -86,6 +90,10 @@ Note: An SSH banner may eventually appear.
Maybe.
"""
+# Faux 'packet type' we do not implement and are unlikely ever to (but which is
+# technically "within spec" re RFC 4251
+MSG_FUGGEDABOUTIT = 253
+
class TransportTest(unittest.TestCase):
# TODO: this can get nuked once ServiceRequestingTransport becomes the
@@ -1302,13 +1310,49 @@ class TestStrictKex:
)
)
- def test_MessageOrderError_raised_on_out_of_order_messages(self):
+ @mark.parametrize(
+ "ptype",
+ (
+ # "normal" but definitely out-of-order message
+ MSG_CHANNEL_OPEN,
+ # Normally ignored, but not in this case
+ MSG_IGNORE,
+ # Normally triggers debug parsing, but not in this case
+ MSG_DEBUG,
+ # Normally ignored, but...you get the idea
+ MSG_UNIMPLEMENTED,
+ # Not real, so would normally trigger us /sending/
+ # MSG_UNIMPLEMENTED, but...
+ MSG_FUGGEDABOUTIT,
+ ),
+ )
+ def test_MessageOrderError_non_kex_messages_in_initial_kex(self, ptype):
+ class AttackTransport(Transport):
+ # Easiest apparent spot on server side which is:
+ # - late enough for both ends to have handshook on strict mode
+ # - early enough to be in the window of opportunity for Terrapin
+ # attack; essentially during actual kex, when the engine is
+ # waiting for things like MSG_KEXECDH_REPLY (for eg curve25519).
+ def _negotiate_keys(self, m):
+ self.clear_to_send_lock.acquire()
+ try:
+ self.clear_to_send.clear()
+ finally:
+ self.clear_to_send_lock.release()
+ if self.local_kex_init is None:
+ # remote side wants to renegotiate
+ self._send_kex_init()
+ self._parse_kex_init(m)
+ # Here, we would normally kick over to kex_engine, but instead
+ # we want the server to send the OOO message.
+ m = Message()
+ m.add_byte(byte_chr(ptype))
+ # rest of packet unnecessary...
+ self._send_message(m)
+
with raises(MessageOrderError):
- with server() as (tc, _):
- # A bit artificial as it's outside kexinit/handshake, but much
- # easier to trigger and still in line with behavior under test
- tc._expect_packet(MSG_KEXINIT)
- tc.open_session()
+ with server(server_transport_factory=AttackTransport) as (tc, _):
+ pass # above should run and except during connect()
def test_SSHException_raised_on_out_of_order_messages_when_not_strict(
self,