Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-asyncssh for openSUSE:Factory
checked in at 2023-11-13 22:18:05
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-asyncssh (Old)
and /work/SRC/openSUSE:Factory/.python-asyncssh.new.17445 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-asyncssh"
Mon Nov 13 22:18:05 2023 rev:25 rq:1124972 version:2.14.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-asyncssh/python-asyncssh.changes
2023-10-05 20:06:09.181215623 +0200
+++
/work/SRC/openSUSE:Factory/.python-asyncssh.new.17445/python-asyncssh.changes
2023-11-13 22:20:41.635111844 +0100
@@ -1,0 +2,16 @@
+Fri Nov 10 12:34:04 UTC 2023 - Dirk Müller <[email protected]>
+
+- update to 2.14.1 (bsc#1217028, CVE-2023-46445):
+ * Hardened AsyncSSH state machine against potential message
+ injection attacks, described in more detail in
+ `CVE-2023-46445 and CVE-2023-46446
+ * Added support for passing in a regex in readuntil in
+ SSHReader,
+ * Added support for get_addresses() and get_port() methods on
+ * SSHAcceptor.
+ * Fixed an issue with AsyncFileWriter potentially writing data
+ * out of order.
+ * Updated testing to include Python 3.12.
+ * Updated readthedocs integration to use YAML config file.
+
+-------------------------------------------------------------------
Old:
----
asyncssh-2.14.0.tar.gz
New:
----
asyncssh-2.14.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-asyncssh.spec ++++++
--- /var/tmp/diff_new_pack.QQbB0w/_old 2023-11-13 22:20:42.303136440 +0100
+++ /var/tmp/diff_new_pack.QQbB0w/_new 2023-11-13 22:20:42.303136440 +0100
@@ -19,7 +19,7 @@
%define skip_python2 1
%define skip_python36 1
Name: python-asyncssh
-Version: 2.14.0
+Version: 2.14.1
Release: 0
Summary: Asynchronous SSHv2 client and server library
License: EPL-2.0 OR GPL-2.0-or-later
++++++ asyncssh-2.14.0.tar.gz -> asyncssh-2.14.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/.github/workflows/run_tests.yml
new/asyncssh-2.14.1/.github/workflows/run_tests.yml
--- old/asyncssh-2.14.0/.github/workflows/run_tests.yml 2023-10-01
02:35:42.000000000 +0200
+++ new/asyncssh-2.14.1/.github/workflows/run_tests.yml 2023-11-09
03:28:14.000000000 +0100
@@ -8,7 +8,7 @@
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+ python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
include:
- os: macos-latest
python-version: "3.10"
@@ -16,6 +16,9 @@
- os: macos-latest
python-version: "3.11"
openssl-version: "3"
+ - os: macos-latest
+ python-version: "3.12"
+ openssl-version: "3"
exclude:
# test hangs on these combination
- os: windows-latest
@@ -26,6 +29,8 @@
python-version: "3.10"
- os: windows-latest
python-version: "3.11"
+ - os: windows-latest
+ python-version: "3.12"
runs-on: ${{ matrix.os }}
env:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/.readthedocs.yaml
new/asyncssh-2.14.1/.readthedocs.yaml
--- old/asyncssh-2.14.0/.readthedocs.yaml 1970-01-01 01:00:00.000000000
+0100
+++ new/asyncssh-2.14.1/.readthedocs.yaml 2023-10-01 04:26:44.000000000
+0200
@@ -0,0 +1,14 @@
+version: 2
+
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.11"
+
+python:
+ install:
+ - requirements: docs/requirements.txt
+
+sphinx:
+ configuration: docs/conf.py
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/PKG-INFO new/asyncssh-2.14.1/PKG-INFO
--- old/asyncssh-2.14.0/PKG-INFO 2023-10-01 03:06:55.014456500 +0200
+++ new/asyncssh-2.14.1/PKG-INFO 2023-11-09 04:02:21.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: asyncssh
-Version: 2.14.0
+Version: 2.14.1
Summary: AsyncSSH: Asynchronous SSHv2 client and server library
Home-page: http://asyncssh.timeheart.net
Author: Ron Frederick
@@ -16,11 +16,12 @@
Classifier: License :: OSI Approved
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: POSIX
-Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -265,3 +266,5 @@
__ http://groups.google.com/d/forum/asyncssh-announce
__ http://groups.google.com/d/forum/asyncssh-dev
__ http://groups.google.com/d/forum/asyncssh-users
+
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh/connection.py
new/asyncssh-2.14.1/asyncssh/connection.py
--- old/asyncssh-2.14.0/asyncssh/connection.py 2023-10-01 02:35:42.000000000
+0200
+++ new/asyncssh-2.14.1/asyncssh/connection.py 2023-11-09 03:28:14.000000000
+0100
@@ -693,13 +693,61 @@
def __getattr__(self, name: str) -> Any:
return getattr(self._server, name)
+ def get_addresses(self) -> List[Tuple]:
+ """Return socket addresses being listened on
+
+ This method returns the socket addresses being listened on.
+ It returns tuples of the form returned by
+ :meth:`socket.getsockname`. If the listener was created
+ using a hostname, the host's resolved IPs will be returned.
+ If the requested listening port was `0`, the selected
+ listening ports will be returned.
+
+ :returns: A list of socket addresses being listened on
+
+ """
+
+ if hasattr(self._server, 'get_addresses'):
+ return self._server.get_addresses()
+ else:
+ return [sock.getsockname() for sock in self.sockets]
+
+ def get_port(self) -> int:
+ """Return the port number being listened on
+
+ This method returns the port number being listened on.
+ If it is listening on multiple sockets with different port
+ numbers, this function will return `0`. In that case,
+ :meth:`get_addresses` can be used to retrieve the full
+ list of listening addresses and ports.
+
+ :returns: The port number being listened on, if there's only one
+
+ """
+
+ if hasattr(self._server, 'get_port'):
+ return self._server.get_port()
+ else:
+ ports = set(addr[1] for addr in self.get_addresses())
+ return ports.pop() if len(ports) == 1 else 0
+
def close(self) -> None:
- """Close this SSH listener"""
+ """Stop listening for new connections
+
+ This method can be called to stop listening for new
+ SSH connections. Existing connections will remain open.
+
+ """
self._server.close()
async def wait_closed(self) -> None:
- """Wait for this SSH listener to close"""
+ """Wait for this listener to close
+
+ This method is a coroutine which waits for this
+ listener to be closed.
+
+ """
await self._server.wait_closed()
@@ -851,6 +899,8 @@
self._can_send_ext_info = False
self._extensions_to_send: 'OrderedDict[bytes, bytes]' = OrderedDict()
+ self._can_recv_ext_info = False
+
self._server_sig_algs: Set[bytes] = set()
self._next_service: Optional[bytes] = None
@@ -860,6 +910,7 @@
self._auth: Optional[Auth] = None
self._auth_in_progress = False
self._auth_complete = False
+ self._auth_final = False
self._auth_methods = [b'none']
self._auth_was_trivial = True
self._username = ''
@@ -1490,15 +1541,25 @@
skip_reason = ''
exc_reason = ''
- if self._kex and MSG_KEX_FIRST <= pkttype <= MSG_KEX_LAST:
- if self._ignore_first_kex: # pragma: no cover
- skip_reason = 'ignored first kex'
- self._ignore_first_kex = False
+ if MSG_KEX_FIRST <= pkttype <= MSG_KEX_LAST:
+ if self._kex:
+ if self._ignore_first_kex: # pragma: no cover
+ skip_reason = 'ignored first kex'
+ self._ignore_first_kex = False
+ else:
+ handler = self._kex
+ else:
+ skip_reason = 'kex not in progress'
+ exc_reason = 'Key exchange not in progress'
+ elif MSG_USERAUTH_FIRST <= pkttype <= MSG_USERAUTH_LAST:
+ if self._auth:
+ handler = self._auth
else:
- handler = self._kex
- elif (self._auth and
- MSG_USERAUTH_FIRST <= pkttype <= MSG_USERAUTH_LAST):
- handler = self._auth
+ skip_reason = 'auth not in progress'
+ exc_reason = 'Authentication not in progress'
+ elif pkttype > MSG_KEX_LAST and not self._recv_encryption:
+ skip_reason = 'invalid request before kex complete'
+ exc_reason = 'Invalid request before key exchange was complete'
elif pkttype > MSG_USERAUTH_LAST and not self._auth_complete:
skip_reason = 'invalid request before auth complete'
exc_reason = 'Invalid request before authentication was complete'
@@ -1531,6 +1592,9 @@
if exc_reason:
raise ProtocolError(exc_reason)
+ if pkttype > MSG_USERAUTH_LAST:
+ self._auth_final = True
+
if self._transport:
self._recv_seq = (seq + 1) & 0xffffffff
self._recv_handler = self._recv_pkthdr
@@ -1548,9 +1612,7 @@
self._send_kexinit()
self._kexinit_sent = True
- if (((pkttype in {MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT} or
- pkttype > MSG_KEX_LAST) and not self._kex_complete) or
- (pkttype == MSG_USERAUTH_BANNER and
+ if ((pkttype == MSG_USERAUTH_BANNER and
not (self._auth_in_progress or self._auth_complete)) or
(pkttype > MSG_USERAUTH_LAST and not self._auth_complete)):
self._deferred_packets.append((pkttype, args))
@@ -1762,9 +1824,11 @@
not self._waiter.cancelled():
self._waiter.set_result(None)
self._wait = None
- else:
- self.send_service_request(_USERAUTH_SERVICE)
+ return
else:
+ self._extensions_to_send[b'server-sig-algs'] = \
+ b','.join(self._sig_algs)
+
self._send_encryption = next_enc_sc
self._send_enchdrlen = 1 if etm_sc else 5
self._send_blocksize = max(8, enc_blocksize_sc)
@@ -1785,17 +1849,18 @@
recv_mac=self._mac_alg_cs.decode('ascii'),
recv_compression=self._cmp_alg_cs.decode('ascii'))
- if first_kex:
- self._next_service = _USERAUTH_SERVICE
-
- self._extensions_to_send[b'server-sig-algs'] = \
- b','.join(self._sig_algs)
-
if self._can_send_ext_info:
self._send_ext_info()
self._can_send_ext_info = False
self._kex_complete = True
+
+ if first_kex:
+ if self.is_client():
+ self.send_service_request(_USERAUTH_SERVICE)
+ else:
+ self._next_service = _USERAUTH_SERVICE
+
self._send_deferred_packets()
def send_service_request(self, service: bytes) -> None:
@@ -2032,18 +2097,25 @@
service = packet.get_string()
packet.check_end()
- if service == self._next_service:
- self.logger.debug2('Accepting request for service %s', service)
+ if self.is_client():
+ raise ProtocolError('Unexpected service request received')
- self.send_packet(MSG_SERVICE_ACCEPT, String(service))
+ if not self._recv_encryption:
+ raise ProtocolError('Service request received before kex complete')
- if (self.is_server() and # pragma: no branch
- not self._auth_in_progress and
- service == _USERAUTH_SERVICE):
- self._auth_in_progress = True
- self._send_deferred_packets()
- else:
- raise ServiceNotAvailable('Unexpected service request received')
+ if service != self._next_service:
+ raise ServiceNotAvailable('Unexpected service in service request')
+
+ self.logger.debug2('Accepting request for service %s', service)
+
+ self.send_packet(MSG_SERVICE_ACCEPT, String(service))
+
+ self._next_service = None
+
+ if service == _USERAUTH_SERVICE: # pragma: no branch
+ self._auth_in_progress = True
+ self._can_recv_ext_info = False
+ self._send_deferred_packets()
def _process_service_accept(self, _pkttype: int, _pktid: int,
packet: SSHPacket) -> None:
@@ -2052,27 +2124,35 @@
service = packet.get_string()
packet.check_end()
- if service == self._next_service:
- self.logger.debug2('Request for service %s accepted', service)
+ if self.is_server():
+ raise ProtocolError('Unexpected service accept received')
- self._next_service = None
+ if not self._recv_encryption:
+ raise ProtocolError('Service accept received before kex complete')
- if (self.is_client() and # pragma: no branch
- service == _USERAUTH_SERVICE):
- self.logger.info('Beginning auth for user %s', self._username)
+ if service != self._next_service:
+ raise ServiceNotAvailable('Unexpected service in service accept')
- self._auth_in_progress = True
+ self.logger.debug2('Request for service %s accepted', service)
- # This method is only in SSHClientConnection
- # pylint: disable=no-member
- cast('SSHClientConnection', self).try_next_auth()
- else:
- raise ServiceNotAvailable('Unexpected service accept received')
+ self._next_service = None
+
+ if service == _USERAUTH_SERVICE: # pragma: no branch
+ self.logger.info('Beginning auth for user %s', self._username)
+
+ self._auth_in_progress = True
+
+ # This method is only in SSHClientConnection
+ # pylint: disable=no-member
+ cast('SSHClientConnection', self).try_next_auth()
def _process_ext_info(self, _pkttype: int, _pktid: int,
packet: SSHPacket) -> None:
"""Process extension information"""
+ if not self._can_recv_ext_info:
+ raise ProtocolError('Unexpected ext_info received')
+
extensions: Dict[bytes, bytes] = {}
self.logger.debug2('Received extension info')
@@ -2198,6 +2278,7 @@
self._decompress_after_auth = self._next_decompress_after_auth
self._next_recv_encryption = None
+ self._can_recv_ext_info = True
else:
raise ProtocolError('New keys not negotiated')
@@ -2225,8 +2306,10 @@
if self.is_client():
raise ProtocolError('Unexpected userauth request')
elif self._auth_complete:
- # Silently ignore requests if we're already authenticated
- pass
+ # Silently ignore additional auth requests after auth succeeds,
+ # until the client sends a non-auth message
+ if self._auth_final:
+ raise ProtocolError('Unexpected userauth request')
else:
if username != self._username:
self.logger.info('Beginning auth for user %s', username)
@@ -2268,7 +2351,7 @@
self._auth = lookup_server_auth(cast(SSHServerConnection, self),
self._username, method, packet)
- def _process_userauth_failure(self, _pkttype: int, pktid: int,
+ def _process_userauth_failure(self, _pkttype: int, _pktid: int,
packet: SSHPacket) -> None:
"""Process a user authentication failure response"""
@@ -2308,10 +2391,9 @@
# pylint: disable=no-member
cast(SSHClientConnection, self).try_next_auth()
else:
- self.logger.debug2('Unexpected userauth failure response')
- self.send_packet(MSG_UNIMPLEMENTED, UInt32(pktid))
+ raise ProtocolError('Unexpected userauth failure response')
- def _process_userauth_success(self, _pkttype: int, pktid: int,
+ def _process_userauth_success(self, _pkttype: int, _pktid: int,
packet: SSHPacket) -> None:
"""Process a user authentication success response"""
@@ -2337,6 +2419,7 @@
self._auth = None
self._auth_in_progress = False
self._auth_complete = True
+ self._can_recv_ext_info = False
if self._agent:
self._agent.close()
@@ -2364,8 +2447,7 @@
self._waiter.set_result(None)
self._wait = None
else:
- self.logger.debug2('Unexpected userauth success response')
- self.send_packet(MSG_UNIMPLEMENTED, UInt32(pktid))
+ raise ProtocolError('Unexpected userauth success response')
def _process_userauth_banner(self, _pkttype: int, _pktid: int,
packet: SSHPacket) -> None:
@@ -4013,7 +4095,7 @@
stderr: ProcessTarget = PIPE,
bufsize: int = io.DEFAULT_BUFFER_SIZE,
send_eof: bool = True, recv_eof: bool = True,
- **kwargs: object) -> SSHClientProcess:
+ **kwargs: object) -> SSHClientProcess[AnyStr]:
"""Create a process on the remote system
This method is a coroutine wrapper around :meth:`create_session`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh/listener.py
new/asyncssh-2.14.1/asyncssh/listener.py
--- old/asyncssh-2.14.0/asyncssh/listener.py 2022-11-20 17:44:58.000000000
+0100
+++ new/asyncssh-2.14.1/asyncssh/listener.py 2023-11-09 03:28:14.000000000
+0100
@@ -184,6 +184,11 @@
return chan, self._session_factory(orig_host, orig_port)
+ def get_addresses(self) -> List[Tuple]:
+ """Return the socket addresses being listened on"""
+
+ return [(self._listen_host, self._listen_port)]
+
def get_port(self) -> int:
"""Return the port number being listened on"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh/process.py
new/asyncssh-2.14.1/asyncssh/process.py
--- old/asyncssh-2.14.0/asyncssh/process.py 2023-08-12 23:12:09.000000000
+0200
+++ new/asyncssh-2.14.1/asyncssh/process.py 2023-10-06 07:36:18.000000000
+0200
@@ -30,8 +30,8 @@
import socket
import stat
from types import TracebackType
-from typing import Any, AnyStr, Callable, Dict, Generic, IO
-from typing import Iterable, Mapping, Optional, Set, TextIO
+from typing import Any, AnyStr, Awaitable, Callable, Dict, Generic, IO
+from typing import Iterable, List, Mapping, Optional, Set, TextIO
from typing import Tuple, Type, TypeVar, Union, cast
from typing_extensions import Protocol
@@ -308,14 +308,33 @@
encoding: Optional[str], errors: str):
super().__init__(encoding, errors, hasattr(file, 'encoding'))
- self._conn = process.channel.get_connection()
+ self._process: 'SSHProcess[AnyStr]' = process
self._file = file
self._needs_close = needs_close
+ self._queue: asyncio.Queue[Optional[AnyStr]] = asyncio.Queue()
+ self._write_task: Optional[asyncio.Task[None]] = \
+ process.channel.get_connection().create_task(self._writer())
+
+ async def _writer(self) -> None:
+ """Process writes to the file"""
+
+ while True:
+ data = await self._queue.get()
+
+ if data is None:
+ self._queue.task_done()
+ break
+
+ await self._file.write(self.encode(data))
+ self._queue.task_done()
+
+ if self._needs_close:
+ await self._file.close()
def write(self, data: AnyStr) -> None:
"""Write data to the file"""
- self._conn.create_task(self._file.write(self.encode(data)))
+ self._queue.put_nowait(data)
def write_eof(self) -> None:
"""Close output file when end of file is received"""
@@ -325,8 +344,10 @@
def close(self) -> None:
"""Stop forwarding data to the file"""
- if self._needs_close:
- self._conn.create_task(self._file.close())
+ if self._write_task:
+ self._write_task = None
+ self._queue.put_nowait(None)
+ self._process.add_cleanup_task(self._queue.join())
class _PipeReader(_UnicodeReader[AnyStr], asyncio.BaseProtocol):
@@ -721,6 +742,8 @@
def __init__(self, *args) -> None:
super().__init__(*args)
+ self._cleanup_tasks: List[Awaitable[None]] = []
+
self._readers: Dict[Optional[int], _ReaderProtocol] = {}
self._send_eof: Dict[Optional[int], bool] = {}
@@ -729,6 +752,20 @@
self._paused_write_streams: Set[Optional[int]] = set()
+ async def __aenter__(self) -> 'SSHProcess[AnyStr]':
+ """Allow SSHProcess to be used as an async context manager"""
+
+ return self
+
+ async def __aexit__(self, _exc_type: Optional[Type[BaseException]],
+ _exc_value: Optional[BaseException],
+ _traceback: Optional[TracebackType]) -> bool:
+ """Wait for a full channel close when exiting the async context"""
+
+ self.close()
+ await self.wait_closed()
+ return False
+
@property
def channel(self) -> SSHChannel[AnyStr]:
"""The channel associated with the process"""
@@ -931,6 +968,11 @@
return bool(self._paused_write_streams) or \
super()._should_pause_reading()
+ def add_cleanup_task(self, task: Awaitable[None]) -> None:
+ """Add a task to run when the process exits"""
+
+ self._cleanup_tasks.append(task)
+
def connection_lost(self, exc: Optional[Exception]) -> None:
"""Handle a close of the SSH channel"""
@@ -1091,6 +1133,9 @@
assert self._chan is not None
await self._chan.wait_closed()
+ for task in self._cleanup_tasks:
+ await task
+
class SSHClientProcess(SSHProcess[AnyStr], SSHClientStreamSession[AnyStr]):
"""SSH client process handler"""
@@ -1105,20 +1150,6 @@
self._stdout: Optional[SSHReader[AnyStr]] = None
self._stderr: Optional[SSHReader[AnyStr]] = None
- async def __aenter__(self) -> 'SSHClientProcess[AnyStr]':
- """Allow SSHProcess to be used as an async context manager"""
-
- return self
-
- async def __aexit__(self, _exc_type: Optional[Type[BaseException]],
- _exc_value: Optional[BaseException],
- _traceback: Optional[TracebackType]) -> bool:
- """Wait for a full channel close when exiting the async context"""
-
- self.close()
- await self._chan.wait_closed()
- return False
-
def _collect_output(self, datatype: DataType = None) -> AnyStr:
"""Return output from the process"""
@@ -1333,7 +1364,7 @@
self._chan.write(input)
self._chan.write_eof()
- await self._chan.wait_closed()
+ await self.wait_closed()
return self.collect_output()
# pylint: enable=redefined-builtin
@@ -1482,20 +1513,6 @@
self._stdout: Optional[SSHWriter[AnyStr]] = None
self._stderr: Optional[SSHWriter[AnyStr]] = None
- async def __aenter__(self) -> 'SSHServerProcess[AnyStr]':
- """Allow SSHProcess to be used as an async context manager"""
-
- return self
-
- async def __aexit__(self, _exc_type: Optional[Type[BaseException]],
- _exc_value: Optional[BaseException],
- _traceback: Optional[TracebackType]) -> bool:
- """Wait for a full channel close when exiting the async context"""
-
- self.close()
- await self._chan.wait_closed()
- return False
-
def _start_process(self, stdin: SSHReader[AnyStr],
stdout: SSHWriter[AnyStr],
stderr: SSHWriter[AnyStr]) -> MaybeAwait[None]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh/stream.py
new/asyncssh-2.14.1/asyncssh/stream.py
--- old/asyncssh-2.14.0/asyncssh/stream.py 2022-12-27 22:30:36.000000000
+0100
+++ new/asyncssh-2.14.1/asyncssh/stream.py 2023-11-09 03:28:14.000000000
+0100
@@ -24,8 +24,8 @@
import inspect
import re
from typing import TYPE_CHECKING, Any, AnyStr, AsyncIterator
-from typing import Callable, Dict, Generic, Iterable
-from typing import List, Optional, Set, Tuple, Union, cast
+from typing import Callable, Dict, Generic, Iterable, List
+from typing import Optional, Pattern, Set, Tuple, Union, cast
from .constants import EXTENDED_DATA_STDERR
from .logging import SSHLogger
@@ -180,17 +180,35 @@
except asyncio.IncompleteReadError as exc:
return cast(AnyStr, exc.partial)
- async def readuntil(self, separator: object) -> AnyStr:
+ async def readuntil(self, separator: object,
+ max_separator_len = 0) -> AnyStr:
"""Read data from the stream until `separator` is seen
This method is a coroutine which reads from the stream until
the requested separator is seen. If a match is found, the
returned data will include the separator at the end.
- The separator argument can be either a single `bytes` or
- `str` value or a sequence of multiple values to match
- against, returning data as soon as any of the separators
- are found in the stream.
+ The `separator` argument can be a single `bytes` or `str`
+ value, a sequence of multiple `bytes` or `str` values,
+ or a compiled regex (`re.Pattern`) to match against,
+ returning data as soon as a matching separator is found
+ in the stream.
+
+ When passing a regex pattern as the separator, the
+ `max_separator_len` argument should be set to the
+ maximum length of an expected separator match. This
+ can greatly improve performance, by minimizing how far
+ back into the stream must be searched for a match.
+ When passing literal separators to match against, the
+ max separator length will be set automatically.
+
+ .. note:: For best results, a separator regex should
+ both begin and end with data which is as
+ unique as possible, and should not start or
+ end with optional or repeated elements.
+ Otherwise, you run the risk of failing to
+ match parts of a separator when it is split
+ across multiple reads.
If EOF or a signal is received before a match occurs, an
:exc:`IncompleteReadError <asyncio.IncompleteReadError>`
@@ -202,7 +220,8 @@
"""
- return await self._session.readuntil(separator, self._datatype)
+ return await self._session.readuntil(separator, self._datatype,
+ max_separator_len)
async def readexactly(self, n: int) -> AnyStr:
"""Read an exact amount of data from the stream
@@ -558,7 +577,8 @@
return result
- async def readuntil(self, separator: object, datatype: DataType) -> AnyStr:
+ async def readuntil(self, separator: object, datatype: DataType,
+ max_separator_len: int) -> AnyStr:
"""Read data from the channel until a separator is seen"""
if not separator:
@@ -570,16 +590,20 @@
if separator is _NEWLINE:
seplen = 1
separators = cast(AnyStr, '\n' if self._encoding else b'\n')
+ pat = re.compile(separators)
elif isinstance(separator, (bytes, str)):
seplen = len(separator)
- separators = re.escape(cast(AnyStr, separator))
+ pat = re.compile(re.escape(cast(AnyStr, separator)))
+ elif isinstance(separator, Pattern):
+ seplen = max_separator_len
+ pat = cast(Pattern[AnyStr], separator)
else:
bar = cast(AnyStr, '|' if self._encoding else b'|')
seplist = list(cast(Iterable[AnyStr], separator))
seplen = max(len(sep) for sep in seplist)
separators = bar.join(re.escape(sep) for sep in seplist)
+ pat = re.compile(separators)
- pat = re.compile(separators)
curbuf = 0
buflen = 0
@@ -602,7 +626,7 @@
newbuf = cast(AnyStr, recv_buf[curbuf])
buf += newbuf
- start = max(buflen + 1 - seplen, 0)
+ start = 0 if seplen == 0 else max(buflen + 1 - seplen, 0)
match = pat.search(buf, start)
if match:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh/version.py
new/asyncssh-2.14.1/asyncssh/version.py
--- old/asyncssh-2.14.0/asyncssh/version.py 2023-10-01 03:04:52.000000000
+0200
+++ new/asyncssh-2.14.1/asyncssh/version.py 2023-11-09 03:30:33.000000000
+0100
@@ -26,4 +26,4 @@
__url__ = 'http://asyncssh.timeheart.net'
-__version__ = '2.14.0'
+__version__ = '2.14.1'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh.egg-info/PKG-INFO
new/asyncssh-2.14.1/asyncssh.egg-info/PKG-INFO
--- old/asyncssh-2.14.0/asyncssh.egg-info/PKG-INFO 2023-10-01
03:06:54.000000000 +0200
+++ new/asyncssh-2.14.1/asyncssh.egg-info/PKG-INFO 2023-11-09
04:02:21.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: asyncssh
-Version: 2.14.0
+Version: 2.14.1
Summary: AsyncSSH: Asynchronous SSHv2 client and server library
Home-page: http://asyncssh.timeheart.net
Author: Ron Frederick
@@ -16,11 +16,12 @@
Classifier: License :: OSI Approved
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: POSIX
-Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -265,3 +266,5 @@
__ http://groups.google.com/d/forum/asyncssh-announce
__ http://groups.google.com/d/forum/asyncssh-dev
__ http://groups.google.com/d/forum/asyncssh-users
+
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh.egg-info/SOURCES.txt
new/asyncssh-2.14.1/asyncssh.egg-info/SOURCES.txt
--- old/asyncssh-2.14.0/asyncssh.egg-info/SOURCES.txt 2023-10-01
03:06:54.000000000 +0200
+++ new/asyncssh-2.14.1/asyncssh.egg-info/SOURCES.txt 2023-11-09
04:02:21.000000000 +0100
@@ -1,5 +1,6 @@
.coveragerc
.gitignore
+.readthedocs.yaml
CONTRIBUTING.rst
COPYRIGHT
LICENSE
@@ -86,6 +87,7 @@
docs/conf.py
docs/contributing.rst
docs/index.rst
+docs/requirements.txt
docs/rtd-req.txt
docs/_templates/sidebarbottom.html
docs/_templates/sidebartop.html
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/docs/api.rst
new/asyncssh-2.14.1/docs/api.rst
--- old/asyncssh-2.14.0/docs/api.rst 2023-10-01 02:35:42.000000000 +0200
+++ new/asyncssh-2.14.1/docs/api.rst 2023-11-09 03:28:14.000000000 +0100
@@ -1038,9 +1038,13 @@
.. autoclass:: SSHAcceptor()
- ====================== =
+ ============================= =
+ .. automethod:: get_addresses
+ .. automethod:: get_port
+ .. automethod:: close
+ .. automethod:: wait_closed
.. automethod:: update
- ====================== =
+ ============================= =
SSHListener
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/docs/changes.rst
new/asyncssh-2.14.1/docs/changes.rst
--- old/asyncssh-2.14.0/docs/changes.rst 2023-10-01 03:04:52.000000000
+0200
+++ new/asyncssh-2.14.1/docs/changes.rst 2023-11-09 03:50:21.000000000
+0100
@@ -3,6 +3,32 @@
Change Log
==========
+Release 2.14.1 (8 Nov 2023)
+---------------------------
+
+* Hardened AsyncSSH state machine against potential message
+ injection attacks, described in more detail in `CVE-2023-46445
+ <https://github.com/advisories/CVE-2023-46445>`_ and `CVE-2023-46446
+ <https://github.com/advisories/CVE-2023-46446>`_. Thanks go to
+ Fabian Bäumer, Marcus Brinkmann, and Jörg Schwenk for identifying
+ and reporting these vulnerabilities and providing detailed analysis
+ and suggestions about the proposed fixes.
+
+* Added support for passing in a regex in readuntil in SSHReader,
+ contributed by Oded Engel.
+
+* Added support for get_addresses() and get_port() methods on
+ SSHAcceptor. Thanks go to Allison Karlitskaya for suggesting
+ this feature.
+
+* Fixed an issue with AsyncFileWriter potentially writing data
+ out of order. Thanks go to Chan Chun Wai for reporting this
+ issue and providing code to reproduce it.
+
+* Updated testing to include Python 3.12.
+
+* Updated readthedocs integration to use YAML config file.
+
Release 2.14.0 (30 Sep 2023)
----------------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/docs/conf.py
new/asyncssh-2.14.1/docs/conf.py
--- old/asyncssh-2.14.0/docs/conf.py 2022-01-23 17:15:42.000000000 +0100
+++ new/asyncssh-2.14.1/docs/conf.py 2023-11-09 03:28:14.000000000 +0100
@@ -44,7 +44,7 @@
# General information about the project.
project = 'AsyncSSH'
-copyright = '2013-2017, ' + __author__
+copyright = '2013-2023, ' + __author__
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -103,7 +103,7 @@
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
- "sidebarwidth": 305,
+ "sidebarwidth": 450,
"stickysidebar": "true"
}
@@ -129,7 +129,7 @@
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+#html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/docs/requirements.txt
new/asyncssh-2.14.1/docs/requirements.txt
--- old/asyncssh-2.14.0/docs/requirements.txt 1970-01-01 01:00:00.000000000
+0100
+++ new/asyncssh-2.14.1/docs/requirements.txt 2023-10-01 04:26:44.000000000
+0200
@@ -0,0 +1,2 @@
+cryptography >= 39.0
+typing_extensions >= 3.6
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/setup.py new/asyncssh-2.14.1/setup.py
--- old/asyncssh-2.14.0/setup.py 2023-10-01 02:35:42.000000000 +0200
+++ new/asyncssh-2.14.1/setup.py 2023-11-09 03:28:14.000000000 +0100
@@ -78,11 +78,12 @@
'License :: OSI Approved',
'Operating System :: MacOS :: MacOS X',
'Operating System :: POSIX',
- 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
+ 'Programming Language :: Python :: 3.11',
+ 'Programming Language :: Python :: 3.12',
'Topic :: Internet',
'Topic :: Security :: Cryptography',
'Topic :: Software Development :: Libraries :: Python Modules',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/tests/test_connection.py
new/asyncssh-2.14.1/tests/test_connection.py
--- old/asyncssh-2.14.0/tests/test_connection.py 2023-10-01
02:35:42.000000000 +0200
+++ new/asyncssh-2.14.1/tests/test_connection.py 2023-11-09
03:28:14.000000000 +0100
@@ -30,11 +30,12 @@
from unittest.mock import patch
import asyncssh
-from asyncssh.constants import MSG_UNIMPLEMENTED, MSG_DEBUG
+from asyncssh.constants import MSG_DEBUG
from asyncssh.constants import MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT
-from asyncssh.constants import MSG_KEXINIT, MSG_NEWKEYS
+from asyncssh.constants import MSG_KEXINIT, MSG_NEWKEYS, MSG_KEX_FIRST
from asyncssh.constants import MSG_USERAUTH_REQUEST, MSG_USERAUTH_SUCCESS
from asyncssh.constants import MSG_USERAUTH_FAILURE, MSG_USERAUTH_BANNER
+from asyncssh.constants import MSG_USERAUTH_FIRST
from asyncssh.constants import MSG_GLOBAL_REQUEST
from asyncssh.constants import MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_CONFIRMATION
from asyncssh.constants import MSG_CHANNEL_OPEN_FAILURE, MSG_CHANNEL_DATA
@@ -337,14 +338,6 @@
return False
-def disconnect_on_unimplemented(self, pkttype, pktid, packet):
- """Process an unimplemented message response"""
-
- # pylint: disable=unused-argument
-
- self.disconnect(asyncssh.DISC_BY_APPLICATION, 'Unexpected response')
-
-
@patch_gss
@patch('asyncssh.connection.SSHClientConnection', _CheckAlgsClientConnection)
class _TestConnection(ServerTestCase):
@@ -974,8 +967,8 @@
with patch('asyncssh.connection.SSHClientConnection.send_newkeys',
send_newkeys):
- async with self.connect():
- pass
+ with self.assertRaises(asyncssh.ProtocolError):
+ await self.connect()
@asynctest
async def test_encryption_algs(self):
@@ -1101,22 +1094,86 @@
await conn.wait_closed()
@asynctest
- async def test_invalid_service_request(self):
- """Test invalid service request"""
+ async def test_service_request_before_kex_complete(self):
+ """Test service request before kex is complete"""
+
+ def send_newkeys(self, k, h):
+ """Finish a key exchange and send a new keys message"""
+
+ self.send_packet(MSG_SERVICE_REQUEST, String('ssh-userauth'))
+
+ asyncssh.connection.SSHConnection.send_newkeys(self, k, h)
+
+ with patch('asyncssh.connection.SSHClientConnection.send_newkeys',
+ send_newkeys):
+ with self.assertRaises(asyncssh.ProtocolError):
+ await self.connect()
+
+ @asynctest
+ async def test_service_accept_before_kex_complete(self):
+ """Test service accept before kex is complete"""
+
+ def send_newkeys(self, k, h):
+ """Finish a key exchange and send a new keys message"""
+
+ self.send_packet(MSG_SERVICE_ACCEPT, String('ssh-userauth'))
+
+ asyncssh.connection.SSHConnection.send_newkeys(self, k, h)
+
+ with patch('asyncssh.connection.SSHServerConnection.send_newkeys',
+ send_newkeys):
+ with self.assertRaises(asyncssh.ProtocolError):
+ await self.connect()
+
+ @asynctest
+ async def test_unexpected_service_name_in_request(self):
+ """Test unexpected service name in service request"""
conn = await self.connect()
conn.send_packet(MSG_SERVICE_REQUEST, String('xxx'))
await conn.wait_closed()
@asynctest
- async def test_invalid_service_accept(self):
- """Test invalid service accept"""
+ async def test_unexpected_service_name_in_accept(self):
+ """Test unexpected service name in accept sent by server"""
+
+ def send_newkeys(self, k, h):
+ """Finish a key exchange and send a new keys message"""
+
+ asyncssh.connection.SSHConnection.send_newkeys(self, k, h)
+
+ self.send_packet(MSG_SERVICE_ACCEPT, String('xxx'))
+
+ with patch('asyncssh.connection.SSHServerConnection.send_newkeys',
+ send_newkeys):
+ with self.assertRaises(asyncssh.ServiceNotAvailable):
+ await self.connect()
+
+ @asynctest
+ async def test_service_accept_from_client(self):
+ """Test service accept sent by client"""
conn = await self.connect()
- conn.send_packet(MSG_SERVICE_ACCEPT, String('xxx'))
+ conn.send_packet(MSG_SERVICE_ACCEPT, String('ssh-userauth'))
await conn.wait_closed()
@asynctest
+ async def test_service_request_from_server(self):
+ """Test service request sent by server"""
+
+ def send_newkeys(self, k, h):
+ """Finish a key exchange and send a new keys message"""
+
+ asyncssh.connection.SSHConnection.send_newkeys(self, k, h)
+
+ self.send_packet(MSG_SERVICE_REQUEST, String('ssh-userauth'))
+
+ with patch('asyncssh.connection.SSHServerConnection.send_newkeys',
+ send_newkeys):
+ with self.assertRaises(asyncssh.ProtocolError):
+ await self.connect()
+
+ @asynctest
async def test_packet_decode_error(self):
"""Test SSH packet decode error"""
@@ -1323,6 +1380,39 @@
await conn.wait_closed()
@asynctest
+ async def test_kex_after_kex_complete(self):
+ """Test kex request when kex not in progress"""
+
+ conn = await self.connect()
+ conn.send_packet(MSG_KEX_FIRST)
+ await conn.wait_closed()
+
+ @asynctest
+ async def test_userauth_after_auth_complete(self):
+ """Test userauth request when auth not in progress"""
+
+ conn = await self.connect()
+ conn.send_packet(MSG_USERAUTH_FIRST)
+ await conn.wait_closed()
+
+ @asynctest
+ async def test_userauth_before_kex_complete(self):
+ """Test receiving userauth before kex is complete"""
+
+ def send_newkeys(self, k, h):
+ """Finish a key exchange and send a new keys message"""
+
+ self.send_packet(MSG_USERAUTH_REQUEST, String('guest'),
+ String('ssh-connection'), String('none'))
+
+ asyncssh.connection.SSHConnection.send_newkeys(self, k, h)
+
+ with patch('asyncssh.connection.SSHClientConnection.send_newkeys',
+ send_newkeys):
+ with self.assertRaises(asyncssh.ProtocolError):
+ await self.connect()
+
+ @asynctest
async def test_invalid_userauth_service(self):
"""Test invalid service in userauth request"""
@@ -1372,24 +1462,31 @@
await asyncio.sleep(0.1)
@asynctest
+ async def test_late_userauth_request(self):
+ """Test userauth request after auth is final"""
+
+ async with self.connect() as conn:
+ conn.send_packet(MSG_GLOBAL_REQUEST, String('xxx'),
+ Boolean(False))
+ conn.send_packet(MSG_USERAUTH_REQUEST, String('guest'),
+ String('ssh-connection'), String('none'))
+ await conn.wait_closed()
+
+ @asynctest
async def test_unexpected_userauth_success(self):
"""Test unexpected userauth success response"""
- with patch.dict('asyncssh.connection.SSHConnection._packet_handlers',
- {MSG_UNIMPLEMENTED: disconnect_on_unimplemented}):
- conn = await self.connect()
- conn.send_packet(MSG_USERAUTH_SUCCESS)
- await conn.wait_closed()
+ conn = await self.connect()
+ conn.send_packet(MSG_USERAUTH_SUCCESS)
+ await conn.wait_closed()
@asynctest
async def test_unexpected_userauth_failure(self):
"""Test unexpected userauth failure response"""
- with patch.dict('asyncssh.connection.SSHConnection._packet_handlers',
- {MSG_UNIMPLEMENTED: disconnect_on_unimplemented}):
- conn = await self.connect()
- conn.send_packet(MSG_USERAUTH_FAILURE, NameList([]),
Boolean(False))
- await conn.wait_closed()
+ conn = await self.connect()
+ conn.send_packet(MSG_USERAUTH_FAILURE, NameList([]), Boolean(False))
+ await conn.wait_closed()
@asynctest
async def test_unexpected_userauth_banner(self):
@@ -2276,8 +2373,7 @@
"""Test using an SSH listener as a context manager"""
async with self.listen() as server:
- listen_port = server.sockets[0].getsockname()[1]
-
+ listen_port = server.get_port()
async with asyncssh.connect('127.0.0.1', listen_port,
known_hosts=(['skey.pub'], [], [])):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/tests/test_forward.py
new/asyncssh-2.14.1/tests/test_forward.py
--- old/asyncssh-2.14.0/tests/test_forward.py 2023-10-01 02:35:42.000000000
+0200
+++ new/asyncssh-2.14.1/tests/test_forward.py 2023-11-09 03:28:14.000000000
+0100
@@ -422,8 +422,10 @@
async with self.connect() as conn:
async with conn.listen_ssh(port=0, server_factory=Server,
- server_host_keys=['skey']) as server2:
- listen_port = server2.get_port()
+ server_host_keys=['skey']) as server:
+ listen_port = server.get_port()
+
+ self.assertEqual(server.get_addresses(), [('', listen_port)])
async with asyncssh.connect('127.0.0.1', listen_port,
known_hosts=(['skey.pub'], [],
[])):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/tests/test_stream.py
new/asyncssh-2.14.1/tests/test_stream.py
--- old/asyncssh-2.14.0/tests/test_stream.py 2022-12-27 22:30:36.000000000
+0100
+++ new/asyncssh-2.14.1/tests/test_stream.py 2023-11-09 03:28:14.000000000
+0100
@@ -21,6 +21,7 @@
"""Unit tests for AsyncSSH stream API"""
import asyncio
+import re
import asyncssh
@@ -392,6 +393,27 @@
stdin.close()
@asynctest
+ async def test_readuntil_regex(self):
+ """Test readuntil with a regex pattern"""
+
+ async with self.connect() as conn:
+ stdin, stdout, _ = await conn.open_session()
+ stdin.write("hello world\nhello world")
+ output = await stdout.readuntil(
+ re.compile('hello world'), len('hello world')
+ )
+ self.assertEqual(output, "hello world")
+
+ output = await stdout.readuntil(
+ re.compile('hello world'), len('hello world')
+ )
+ self.assertEqual(output, "\nhello world")
+
+ stdin.close()
+
+ await conn.wait_closed()
+
+ @asynctest
async def test_abort(self):
"""Test abort on a channel"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asyncssh-2.14.0/tox.ini new/asyncssh-2.14.1/tox.ini
--- old/asyncssh-2.14.0/tox.ini 2023-10-01 02:35:42.000000000 +0200
+++ new/asyncssh-2.14.1/tox.ini 2023-11-09 03:28:14.000000000 +0100
@@ -4,7 +4,7 @@
envlist =
clean
report
- py3{7,8,9,10,11}-{linux,darwin,windows}
+ py3{7,8,9,10,11,12}-{linux,darwin,windows}
[testenv]
deps =
@@ -53,7 +53,7 @@
coverage html
coverage xml
depends =
- py3{7,8,9,10,11}-{linux,darwin,windows}
+ py3{7,8,9,10,11,12}-{linux,darwin,windows}
[pytest]
testpaths = tests