| विवरण | S2OPC 1.7.3 contains a client-side out-of-bounds read in the alarm and conditions wrapper when processing event notifications received through a normal OPC UA PublishResponse path. The issue is reachable in the official client wrapper alarm handling flow with event management enabled, and it does not require misuse of undocumented APIs or direct invocation of low-level parsers. In the tested setup, the client was the official sample-based wrapper harness, the server was the real toolkit_demo_server_alarms program, and the transport path remained a valid OPC UA session and subscription sequence. The defect appears after the client creates the alarm monitored item, requests a ConditionRefresh2 operation, and later receives a PublishResponse carrying an EventNotificationList. If a malicious OPC UA server, or an active man-in-the-middle able to alter the server response in transit, increases the field count of a returned EventFieldList beyond the number of select clauses that the client originally stored locally, the wrapper reads beyond the bounds of its local alarm_selectClauses pointer array and subsequently dereferences an invalid pointer during event path resolution.
The vulnerable logic is in the client wrapper alarm processing code, especially in SOPC_MonitoredAlarm_TriggerSubscriptionNotification in src/ClientServer/frontend/client_wrapper/alarm_conditions/libs2opc_client_alarm_conditions.c. During alarm group creation, the wrapper builds an event filter from its own local alarm event model. It computes the number of expected variables by calling SOPC_Event_GetNbVariables on the event structure, allocates the local alarm_selectClauses array, and fills it through SOPC_Event_ForEachVar. As a result, alarm_selectClauses_len reflects the number of locally expected event fields represented as browse-path-like strings, and this array length remains fixed after monitored item creation. Later, when a PublishResponse is received, the state machine passes the decoded EventNotificationList into the alarm callback chain through LockedStaMac_ProcessMsg_PublishResponse and LockedStaMac_ProcessMsg_PubResp_EventNotifList in src/ClientServer/frontend/client_wrapper/internal/state_machine.c. At that point, the callback validates the number of event notification elements, but it does not validate that each event's NoOfEventFields remains consistent with the number of locally stored select clauses.
The core bug is caused by trusting the remote event->NoOfEventFields value as the loop bound for field processing while indexing a separate local array whose size is controlled only by the original client-side filter construction. The callback iterates over all fields from index 0 to event->NoOfEventFields - 1. Field zero is treated specially as the ConditionId field. For each subsequent field, the code derives a local clause string by reading MAgroup->alarm_selectClauses[iField - 1]. There is no boundary check ensuring that iField - 1 is smaller than MAgroup->alarm_selectClauses_len before this access occurs. Therefore, if the server reports more event fields than the client originally requested and stored, the callback eventually reads alarm_selectClauses out of bounds. In the reproduced case, GDB showed alarm_selectClauses_len equal to 46, NoOfEventFields equal to 48, and i equal to 47 immediately before the invalid access, meaning the code was about to read alarm_selectClauses[46] even though the valid indexes were only 0 through 45. The value obtained at that position was 0xbebebebebebebebe, consistent with poisoned or uninitialized memory patterns.
The out-of-bounds read is then amplified because the invalid pointer read from the local array is treated as a qualified-name path string and passed into SOPC_Event_SetVariableFromStrPath in src/ClientServer/address_space/sopc_event_manager.c. That function continues into dictionary lookup and string hashing operations, ultimately reaching SOPC_Dict_Get, str_hash, and strlen. Because the pointer is invalid, the client crashes with a read access violation. The observed AddressSanitizer output showed a deadly signal caused by a READ memory access, with the relevant stack frames including __interceptor_strlen, str_hash at sopc_event_manager.c:1013, SOPC_Dict_Get, SOPC_Event_SetVariableFromStrPath at sopc_event_manager.c:667, SOPC_MonitoredAlarm_TriggerSubscriptionNotification at libs2opc_client_alarm_conditions.c:396 and :323, and the publish response processing path in state_machine.c at lines 1967 and 2084. This confirms that the crash happens in the official wrapper client while consuming a real PublishResponse event notification.
The reproduction used S2OPC 1.7.3 built from commit b4c5c7d63cd69698461d514b905a7c92b3c377c4 with S2OPC_EVENT_MANAGEMENT enabled. The real toolkit_demo_server_alarms server was launched with the provided demo XML files. A protocol-aware proxy was placed between the client and server. The proxy did not short-circuit the OPC UA workflow. It allowed the normal handshake, OpenSecureChannel, CreateSession, ActivateSession, CreateSubscription, CreateMonitoredItems, Call, and Publish traffic to pass, and modified only one real PublishResponse carrying event notification data. Specifically, it increased the first data-bearing EventFieldList.NoOfEventFields by one and appended a Null Variant to the EventFields array so that the message remained decodable. The server log showed normal startup. The proxy log showed a normal request/response sequence, followed by a message indicating that the fifth PublishResponse occurrence with notification data had been mutated by adding one extra event field. The client log then showed successful alarm group creation, a refresh request, and an AddressSanitizer crash in the event processing path. This demonstrates that the defect is reachable through a realistic network path rather than through direct parser fuzzing.
The security impact is a reliable client-side denial of service. A malicious OPC UA server that can establish a session and return event notifications can crash a S2OPC-based client application using the official alarm and conditions wrapper. An on-path attacker who can modify PublishResponse traffic can also exploit the flaw by inflating NoOfEventFields after the session has been established. The demonstrated primitive is an out-of-bounds read followed by invalid pointer dereference. The available evidence supports stable remote client process termination, but it does not justify a stronger claim such as remote code execution, because no controlled write primitive or instruction pointer control has been demonstrated. The issue should therefore be classified primarily as a remotely triggerable client-side denial of service with clear memory-safety evidence.
This behavior is not explained by API misuse. The reproduction relies on the normal high-level wrapper flow: initialization, connection, alarm group creation, monitored item creation, and ConditionRefresh2 request, followed by ordinary PublishResponse handling through the internal state machine. The mismatch is introduced entirely by remote data inside a valid notification path. Setup-time validation of the monitored item filter does not protect the later notification path against a server-provided field-count mismatch.
A robust fix should be implemented in the notification handling logic before any access to alarm_selectClauses[iField - 1]. At minimum, the wrapper should check that every non-zero field index satisfies iField - 1 < MAgroup->alarm_selectClauses_len. If the condition fails, the event should be treated as invalid or protocol-inconsistent, a diagnostic should be logged, and the offending event should be discarded without passing an invalid clause pointer into the event manager. A stronger defensive improvement would also verify that event->NoOfEventFields does not exceed 1 + MAgroup->alarm_selectClauses_len for ordinary alarm event processing, while still preserving any intentionally handled special cases defined by the wrapper design.
In summary, the vulnerability arises from a structural inconsistency between a remotely controlled field count in PublishResponse event data and a locally allocated select-clause pointer array created earlier during alarm group setup. The implementation trusts the remote field count, uses it as the loop bound, and then indexes the local array without enforcing a corresponding upper bound. This leads to an out-of-bounds read in the client, after which the resulting invalid pointer is treated as a string path and dereferenced in downstream event management code, causing a crash. The issue is reproducible against the official S2OPC 1.7.3 alarm demo server and client wrapper path, requires no local misuse of the library API, and has practical security relevance because it allows a malicious server or traffic-modifying intermediary to terminate affected client processes remotely.
|
|---|