| Description | I identified a server-side heap out-of-bounds read in S2OPC Toolkit 1.7.3, reproduced on v1.7.3, in the Republish handling path of the OPC UA Client/Server stack. In my reproduction, the bug is reached through a real Subscription/Publish/Republish interaction against the official validation server target built from the upstream repository. The vulnerable condition appears when the server first generates a valid mixed notification message containing both a DataChangeNotification and an EventNotificationList, and later rebuilds that message into a RepublishResponse. At that point, the implementation preserves the original NotificationMessage element count but re-allocates backing storage for only one SOPC_ExtensionObject. The result is an internal object whose logical cardinality and physical storage are inconsistent. During response serialization, the encoder trusts the element count and reads a second notification element that was never allocated in the rebuilt response object. In AddressSanitizer builds, this terminates the server with a heap-buffer-overflow read in SOPC_ExtensionObject_Write.
The root cause can be explained directly from the upstream 1.7.3 sources. In src/ClientServer/services/b2c/msg_subscription_publish_bs.c, the normal Publish path allocates NotificationMessage content. The function msg_subscription_publish_bs__alloc_notification_message_items() initializes notifMsg->NoOfNotificationData to 1, then explicitly upgrades it to 2 when both data notifications and event notifications are present in the same publication batch. The code comment says “1 for each type”, and the function allocates notifMsg->NotificationData with SOPC_Calloc((size_t) notifMsg->NoOfNotificationData, sizeof(SOPC_ExtensionObject)). The same file later relies on that design: the event side accepts NotificationData[1] when NoOfNotificationData == 2 and the first slot is occupied by a DataChangeNotification. A two-element NotificationData array is therefore legitimate server-generated state produced by the ordinary Publish implementation whenever one notification message carries both monitored item data and event fields.
The flaw is introduced in src/ClientServer/services/b2c/msg_subscription_publish_ack_bs.c in msg_subscription_publish_ack_bs__setall_msg_republish_response(). The function first copies the whole source NotificationMessage into the response with a plain structure assignment. This shallow copy preserves NoOfNotificationData and the source NotificationData pointer. Immediately afterward, however, the function replaces resp->NotificationMessage.NotificationData with a fresh heap allocation of exactly one SOPC_ExtensionObject via SOPC_Malloc(1 * sizeof(SOPC_ExtensionObject)). It then initializes that single element and deep-copies only NotificationData[0] from the source message. There is no loop over the source element count, no allocation based on NoOfNotificationData, and no normalization of NoOfNotificationData back to 1. Therefore, if the original published notification legitimately contained two extension objects, the RepublishResponse ends up in a broken state: NoOfNotificationData remains 2 while NotificationData points to a one-element heap region. That mismatch is the vulnerability.
The crash occurs later in the generic encoder. During response serialization, the code in src/Common/opcua_types/sopc_encoder.c eventually reaches SOPC_ExtensionObject_Write through SOPC_Write_Array and the normal encodeable-object machinery. The encoder uses the array length embedded in the message object and iterates over two elements because NoOfNotificationData still equals 2. The first element is valid because it was copied into the newly allocated one-element array. The second iteration computes the address of NotificationData[1], which lies outside the allocated heap region, and reads extObj->Encoding. In my GDB-assisted analysis, sizeof(SOPC_ExtensionObject) was 72 bytes and the Encoding field offset was 48 bytes, which matches the AddressSanitizer report: the invalid read occurs 48 bytes to the right of a 72-byte region allocated in msg_subscription_publish_ack_bs__setall_msg_republish_response(). This is why the sanitizer backtrace points to SOPC_ExtensionObject_Write as the failing access and to the single-element allocation in the Republish response builder as the originating allocation site.
I validated the issue using the official validation server target defined by the upstream CMake configuration. In the 1.7.3 tree, tests/ClientServer/CMakeLists.txt defines toolkit_test_server from tests/ClientServer/validation_tests/server/toolkit_test_server.c and tests/ClientServer/validation_tests/server/toolkit_test_server_alarms.c. To exercise the vulnerable path, I built S2OPC with client/server support, tests, samples, ASan, and event management enabled. The server was launched as “toolkit_test_server events”, which matters because the mixed-notification state depends on event support being active. On the client side, I used a minimal compliant harness placed in the validation test area to establish a session, create a subscription, create one data monitored item and one event monitored item, drain the first publication, trigger an event, and then issue a RepublishRequest for the sequence number associated with a mixed PublishResponse. The client-side output showed a real two-element notification message before the failure: a PublishResponse containing notifData=2, with notif[0] identified as DataChangeNotification and notif[1] identified as EventNotificationList. Immediately after requesting Republish for that sequence number, the server aborted with AddressSanitizer: heap-buffer-overflow.
My GDB analysis confirms that the source notification is valid before the faulty rebuild and that the inconsistency is created entirely on the server. When breaking in msg_subscription_publish_bs__alloc_notification_message_items(), I observed a nonzero data notification count and a nonzero event notification count, then saw notifMsg->NoOfNotificationData change from 1 to 2 inside the mixed-notification branch. After allocation, notifMsg->NotificationData pointed to a properly sized two-element array. When breaking later in msg_subscription_publish_ack_bs__setall_msg_republish_response(), I observed that the source notification still had NoOfNotificationData == 2 and two distinct object types: DataChangeNotification in the first slot and EventNotificationList in the second. After the shallow copy into the response, the response still reflected a two-element notification array. After stepping over the fixed-size SOPC_Malloc(1 * sizeof(SOPC_ExtensionObject)), the response still reported NoOfNotificationData == 2, but NotificationData had been redirected to a fresh one-element heap allocation. At that point the vulnerability was already fully formed: the response object advertised two elements while owning space for only one. The subsequent serialization crash was a deterministic consequence of earlier state corruption, not a separate bug in the encoder.
From a security perspective, the confirmed impact is remote denial of service against the server process. A network client capable of performing normal OPC UA session and subscription operations can drive the server into an out-of-bounds read and crash it by following a semantically valid request sequence. This matters because the trigger is not an invalid binary blob rejected at the edge of parsing; it is a standards-aligned application workflow that reaches deep into the server’s subscription and republish logic. In industrial or OT deployments that use an OPC UA gateway, edge connector, event concentrator, or supervisory integration component built on S2OPC, a crash in this code path can interrupt telemetry, event forwarding, and operator visibility. I am deliberately conservative here: my reproduction conclusively demonstrates server termination, but I am not claiming proven confidentiality impact from data exfiltration because I did not rely on a non-ASan leak demonstration to establish the issue.
This bug should be treated as a real implementation flaw in the upstream code. The two-element mixed notification is created by the official Publish implementation itself, and the official validation server target is sufficient to host the vulnerable state. The Republish path then mishandles that legitimate object by shrinking storage without updating the count. The minimal fix is straightforward: allocate the RepublishResponse NotificationData array according to the source NoOfNotificationData value, initialize each element, deep-copy every source SOPC_ExtensionObject, and roll back cleanly on partial failure. The issue is best described as a server-side heap-based out-of-bounds read in S2OPC RepublishResponse handling caused by a cardinality mismatch between NotificationMessage.NoOfNotificationData and the actual NotificationData backing store after rebuilding a mixed notification message. |
|---|