CVE-2023-26151: Loop with Unreachable Exit Condition ('Infinite Loop') (CWE-835)
value: Versions of the package asyncua before 0.9.96 are vulnerable to Denial of Service (DoS) such that an attacker can send a malformed packet and as a result, the server will enter into an infinite loop and consume excessive memory.
CWE description: The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.
CVSS v3 base score: 7.5
Published: 2023-10-03T05:15Z
Repository: https://github.com/FreeOpcUa/opcua-asyncio
Language: Python
Files changed: asyncua/server/binary_server_asyncio.py, tests/test_connections.py
Commit date: 2022-09-16 19:31:19 +0200
Commit message: server handle malformed packets

fix test after rebase

server handle malformed packets

close connection on error

reorder imports

change test

Diff: diff --git a/asyncua/server/binary_server_asyncio.py b/asyncua/server/binary_server_asyncio.py
index 4afeeafc..c1d434c7 100644
--- a/asyncua/server/binary_server_asyncio.py
+++ b/asyncua/server/binary_server_asyncio.py
@@ -63,19 +63,26 @@ class OPCUAProtocol(asyncio.Protocol):
             try:
                 buf = Buffer(self._buffer)
                 try:
                     header = header_from_binary(buf)
                 except NotEnoughData:
-                    logger.debug('Not enough data while parsing header from client, waiting for more')
+                    # a packet should at least contain a header otherwise it is malformed (8 or 12 bytes)
+                    logger.debug('Not enough data while parsing header from client, empty the buffer')
+                    self.transport.close()
                     return
-                if len(buf) < header.body_size:
-                    logger.debug('We did not receive enough data from client. Need %s got %s', header.body_size,
-                                 len(buf))
-                    return
-                # we have a complete message
-                self.messages.put_nowait((header, buf))
-                self._buffer = self._buffer[(header.header_size + header.body_size):]
+                if header.header_size + header.body_size <= header.header_size:
+                    # malformed header prevent invalid access of your buffer
+                    logger.error(f'Got malformed header {header}')
+                    self.transport.close()
+                else:
+                    if len(buf) < header.body_size:
+                        logger.debug('We did not receive enough data from client. Need %s got %s', header.body_size,
+                                    len(buf))
+                        return
+                    # we have a complete message
+                    self.messages.put_nowait((header, buf))
+                    self._buffer = self._buffer[(header.header_size + header.body_size):]
             except Exception:
                 logger.exception('Exception raised while parsing message from client')
                 return
 
     async def _process_received_message_loop(self):
diff --git a/tests/test_connections.py b/tests/test_connections.py
index 1d1dce4c..4f706370 100644
--- a/tests/test_connections.py
+++ b/tests/test_connections.py
@@ -1,12 +1,13 @@
 # coding: utf-8
 import asyncio
 import pytest
+import asyncio
+import struct
 
-from asyncua import Client, Server
+from asyncua import Client, Server, ua
 from asyncua.ua.uaerrors import BadMaxConnectionsReached
-
 from .conftest import port_num, find_free_port
 
 pytestmark = pytest.mark.asyncio
 
 
@@ -24,10 +25,25 @@ async def test_max_connections_1(opc):
                 async with Client(f'opc.tcp://127.0.0.1:{port}'):
                     pass
     opc.server.iserver.isession.__class__.max_connections = 1000
 
 
+async def test_dos_server(opc):
+    # See issue 1013 a crafted packet triggered dos
+    port = opc.server.endpoint.port
+    async with Client(f'opc.tcp://127.0.0.1:{port}') as c:
+        # craft invalid packet that trigger dos
+        message_type, chunk_type, packet_size = [ua.MessageType.SecureOpen, b'E', 0]
+        c.uaclient.protocol.transport.write(struct.pack("<3scI", message_type, chunk_type, packet_size))
+        # sleep to give the server time to handle the message because we bypass the asyncio
+        await asyncio.sleep(1.0)
+        with pytest.raises(ConnectionError):
+            # now try to read a value to see if server is still alive
+            server_time_node = c.get_node(ua.NodeId(ua.ObjectIds.Server_ServerStatus_CurrentTime))
+            await server_time_node.read_value()
+
+
 async def test_safe_disconnect():
     c = Client(url="opc.tcp://example:4840")
     await c.disconnect()
     # second disconnect should be noop
     await c.disconnect()

Vulnerable code:                     logger.debug('Not enough data while parsing header from client, waiting for more')
                if len(buf) < header.body_size:
                    logger.debug('We did not receive enough data from client. Need %s got %s', header.body_size,
                                 len(buf))
                    return
                # we have a complete message
                self.messages.put_nowait((header, buf))
                self._buffer = self._buffer[(header.header_size + header.body_size):]
from asyncua import Client, Server

Fixed code:                     # a packet should at least contain a header otherwise it is malformed (8 or 12 bytes)
                    logger.debug('Not enough data while parsing header from client, empty the buffer')
                    self.transport.close()
                if header.header_size + header.body_size <= header.header_size:
                    # malformed header prevent invalid access of your buffer
                    logger.error(f'Got malformed header {header}')
                    self.transport.close()
                else:
                    if len(buf) < header.body_size:
                        logger.debug('We did not receive enough data from client. Need %s got %s', header.body_size,
                                    len(buf))
                        return
                    # we have a complete message
                    self.messages.put_nowait((header, buf))
                    self._buffer = self._buffer[(header.header_size + header.body_size):]
import asyncio
import struct
from asyncua import Client, Server, ua
async def test_dos_server(opc):
    # See issue 1013 a crafted packet triggered dos
    port = opc.server.endpoint.port
    async with Client(f'opc.tcp://127.0.0.1:{port}') as c:
        # craft invalid packet that trigger dos
        message_type, chunk_type, packet_size = [ua.MessageType.SecureOpen, b'E', 0]
        c.uaclient.protocol.transport.write(struct.pack("<3scI", message_type, chunk_type, packet_size))
        # sleep to give the server time to handle the message because we bypass the asyncio
        await asyncio.sleep(1.0)
        with pytest.raises(ConnectionError):
            # now try to read a value to see if server is still alive
            server_time_node = c.get_node(ua.NodeId(ua.ObjectIds.Server_ServerStatus_CurrentTime))
            await server_time_node.read_value()