CVE-2018-7750: Improper Authentication (CWE-287)
value: transport.py in the SSH server implementation of Paramiko before 1.17.6, 1.18.x before 1.18.5, 2.0.x before 2.0.8, 2.1.x before 2.1.5, 2.2.x before 2.2.3, 2.3.x before 2.3.2, and 2.4.x before 2.4.1 does not properly check whether authentication is completed before processing other requests, as demonstrated by channel-open. A customized SSH client can simply skip the authentication step.
CWE description: When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
CVSS v3 base score: 9.8
Published: 2018-03-13T18:29Z
Repository: https://github.com/paramiko/paramiko
Language: Python
Files changed: paramiko/common.py, paramiko/transport.py
Commit date: 2018-03-12 17:41:30 -0700
Commit message: Fixes CVE-2018-7750 / #1175

At least, insofar as the new tests pass...!

Diff: diff --git a/paramiko/common.py b/paramiko/common.py
index 0012372..11c4121 100644
--- a/paramiko/common.py
+++ b/paramiko/common.py
@@ -30,10 +30,11 @@ MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \
 MSG_USERAUTH_PK_OK = 60
 MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE = range(60, 62)
 MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN = range(60, 62)
 MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, MSG_USERAUTH_GSSAPI_ERROR,\
     MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC = range(63, 67)
+HIGHEST_USERAUTH_MESSAGE_ID = 79
 MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE = range(80, 83)
 MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \
     MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA, \
     MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST, \
     MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE = range(90, 101)
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 711f395..3ec3f0b 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -47,11 +47,11 @@ from paramiko.common import (
     MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE, MSG_CHANNEL_OPEN_SUCCESS,
     MSG_CHANNEL_OPEN_FAILURE, MSG_CHANNEL_OPEN, MSG_CHANNEL_SUCCESS,
     MSG_CHANNEL_FAILURE, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA,
     MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_REQUEST, MSG_CHANNEL_EOF,
     MSG_CHANNEL_CLOSE, MIN_WINDOW_SIZE, MIN_PACKET_SIZE, MAX_WINDOW_SIZE,
-    DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE,
+    DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE, HIGHEST_USERAUTH_MESSAGE_ID,
 )
 from paramiko.compress import ZlibCompressor, ZlibDecompressor
 from paramiko.dsskey import DSSKey
 from paramiko.kex_gex import KexGex, KexGexSHA256
 from paramiko.kex_group1 import KexGroup1
@@ -1758,10 +1758,47 @@ class Transport(threading.Thread, ClosingContextManager):
     def _sanitize_packet_size(self, max_packet_size):
         if max_packet_size is None:
             max_packet_size = self.default_max_packet_size
         return clamp_value(MIN_PACKET_SIZE, max_packet_size, MAX_WINDOW_SIZE)
 
+    def _ensure_authed(self, ptype, message):
+        """
+        Checks message type against current auth state.
+
+        If server mode, and auth has not succeeded, and the message is of a
+        post-auth type (channel open or global request) an appropriate error
+        response Message is crafted and returned to caller for sending.
+
+        Otherwise (client mode, authed, or pre-auth message) returns None.
+        """
+        if (
+            not self.server_mode
+            or ptype <= HIGHEST_USERAUTH_MESSAGE_ID
+            or self.is_authenticated()
+        ):
+            return None
+        # WELP. We must be dealing with someone trying to do non-auth things
+        # without being authed. Tell them off, based on message class.
+        reply = Message()
+        # Global requests have no details, just failure.
+        if ptype == MSG_GLOBAL_REQUEST:
+            reply.add_byte(cMSG_REQUEST_FAILURE)
+        # Channel opens let us reject w/ a specific type + message.
+        elif ptype == MSG_CHANNEL_OPEN:
+            kind = message.get_text()
+            chanid = message.get_int()
+            reply.add_byte(cMSG_CHANNEL_OPEN_FAILURE)
+            reply.add_int(chanid)
+            reply.add_int(OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED)
+            reply.add_string('')
+            reply.add_string('en')
+        # NOTE: Post-open channel messages do not need checking; the above will
+        # reject attemps to open channels, meaning that even if a malicious
+        # user tries to send a MSG_CHANNEL_REQUEST, it will simply fall under
+        # the logic that handles unknown channel IDs (as the channel list will
+        # be empty.)
+        return reply
 
     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
         # indefinitely, creating a GC cycle and not letting Transport ever be
@@ -1818,11 +1855,15 @@ class Transport(threading.Thread, ClosingContextManager):
                         if (ptype >= 30) and (ptype <= 41):
                             self.kex_engine.parse_next(ptype, m)
                             continue
 
                     if ptype in self._handler_table:
-                        self._handler_table[ptype](self, m)
+                        error_msg = self._ensure_authed(ptype, m)
+                        if error_msg:
+                            self._send_message(error_msg)
+                        else:
+                            self._handler_table[ptype](self, m)
                     elif ptype in self._channel_handler_table:
                         chanid = m.get_int()
                         chan = self._channels.get(chanid)
                         if chan is not None:
                             self._channel_handler_table[ptype](chan, m)

Vulnerable code:     DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE,
                        self._handler_table[ptype](self, m)
Fixed code: HIGHEST_USERAUTH_MESSAGE_ID = 79
    DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE, HIGHEST_USERAUTH_MESSAGE_ID,
    def _ensure_authed(self, ptype, message):
        """
        Checks message type against current auth state.

        If server mode, and auth has not succeeded, and the message is of a
        post-auth type (channel open or global request) an appropriate error
        response Message is crafted and returned to caller for sending.

        Otherwise (client mode, authed, or pre-auth message) returns None.
        """
        if (
            not self.server_mode
            or ptype <= HIGHEST_USERAUTH_MESSAGE_ID
            or self.is_authenticated()
        ):
            return None
        # WELP. We must be dealing with someone trying to do non-auth things
        # without being authed. Tell them off, based on message class.
        reply = Message()
        # Global requests have no details, just failure.
        if ptype == MSG_GLOBAL_REQUEST:
            reply.add_byte(cMSG_REQUEST_FAILURE)
        # Channel opens let us reject w/ a specific type + message.
        elif ptype == MSG_CHANNEL_OPEN:
            kind = message.get_text()
            chanid = message.get_int()
            reply.add_byte(cMSG_CHANNEL_OPEN_FAILURE)
            reply.add_int(chanid)
            reply.add_int(OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED)
            reply.add_string('')
            reply.add_string('en')
        # NOTE: Post-open channel messages do not need checking; the above will
        # reject attemps to open channels, meaning that even if a malicious
        # user tries to send a MSG_CHANNEL_REQUEST, it will simply fall under
        # the logic that handles unknown channel IDs (as the channel list will
        # be empty.)
        return reply
                        error_msg = self._ensure_authed(ptype, m)
                        if error_msg:
                            self._send_message(error_msg)
                        else:
                            self._handler_table[ptype](self, m)