CVE-2023-26151 (published 2023-10-03T05:15Z)

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.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-835
cwe_name: Loop with Unreachable Exit Condition ('Infinite Loop')
description: The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.
extended_description: If the loop can be influenced by an attacker, this weakness could allow attackers to consume excessive resources such as CPU or memory.
         

Fix information:
repo_url: https://github.com/FreeOpcUa/opcua-asyncio
commit_details: repo_url: https://github.com/FreeOpcUa/opcua-asyncio
author: Alexander Schrode
author_date: 2022-08-31 18:00:09+02:00
msg: server handle malformed packets

fix test after rebase

server handle malformed packets

close connection on error

reorder imports

change test
repository: repo_url: https://github.com/FreeOpcUa/opcua-asyncio
repo_name: FreeOpcUa/opcua-asyncio
description: OPC UA library for python >= 3.7
repo_language: Python
owner: FreeOpcUa
file_changes: filename: binary_server_asyncio.py
change_type: ModificationType.MODIFY
diff: @@ -65,15 +65,22 @@ class OPCUAProtocol(asyncio.Protocol):
                 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

programming_language: Python
filename: test_connections.py
change_type: ModificationType.MODIFY
diff: @@ -1,10 +1,11 @@
 # 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
@@ -26,6 +27,21 @@ async def test_max_connections_1(opc):
     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()

programming_language: Python