| 제목 | Systerel S2OPC 1.7.3 Heap-based Out-of-Bounds Read |
|---|
| 설명 | I discovered a server-side memory safety vulnerability in S2OPC version 1.7.3, commit b4c5c7d63cd69698461d514b905a7c92b3c377c4. The issue affects the OPC UA AddNodes service when the library is compiled with S2OPC_NODE_MANAGEMENT enabled. In that configuration, a remote OPC UA client can send a malformed AddNodesRequest that declares NodeClass=Variable and a valid Variable type definition, but intentionally encodes the NodeAttributes ExtensionObject as the generic OpcUa_NodeAttributes structure instead of the specialized OpcUa_VariableAttributes structure expected for a Variable node.
The vulnerability is not caused by misuse of a local API. It is reachable through the normal server-side protocol handling path for AddNodes. In my reproduction, the victim is the project’s own validation server target, toolkit_test_server, and the trigger is a minimal client PoC that sends a single malformed AddNodes request. The crash happens before a normal AddNodes response is produced, so the server process terminates and the client only observes a failed synchronous service result.
At the implementation level, the root cause is a combination of overly permissive type validation and an unsafe downcast in the AddNodes handling logic. In src/ClientServer/services/b2c/address_space_bs.c, the helper that validates the NodeAttributes type for an AddNodes item does not require strict type correspondence between NodeClass and the concrete attribute structure. For a Variable node, the expected type should be OpcUa_VariableAttributes. However, the validation logic also accepts the generic OpcUa_NodeAttributes encodeable type as valid. As a result, a request that is semantically “add a Variable node” but syntactically carries only the smaller generic attribute object is accepted and forwarded into the Variable-specific code path.
The second part of the bug is the unsafe reinterpretation of the accepted object. In the Variable-specific AddNodes path, the server takes address_space_bs__p_nodeAttributes->Body.Object.Value and passes it forward as a const OpcUa_VariableAttributes*. That cast is performed even when the runtime object is still actually an OpcUa_NodeAttributes instance. No conversion, deep copy, normalization, or size check is performed before the pointer is used as though it referenced the larger Variable-specific structure. This creates a classic type confusion condition inside the server: the static type becomes OpcUa_VariableAttributes*, but the dynamic object in memory remains a smaller generic NodeAttributes object.
The helper in src/ClientServer/address_space/internal/sopc_node_mgt_helper_internal.c then processes that pointer. The early part of the function can appear to work because OpcUa_NodeAttributes and OpcUa_VariableAttributes share a common prefix layout for the generic attributes. The helper first handles common fields through logic that is compatible with the shared prefix. The problem appears immediately afterward, when the code starts reading members that exist only in OpcUa_VariableAttributes, including AccessLevel, UserAccessLevel, ArrayDimensions, DataType, MinimumSamplingInterval, Historizing, ValueRank, and Value. At that point the code is no longer operating within the bounds of the actual allocated object.
In my local GDB analysis, the decoded object passed down from the request still had the dynamic type name “NodeAttributes” at the crash sink, proving that it was not transformed into a true OpcUa_VariableAttributes object. The size mismatch was clear: sizeof(OpcUa_NodeAttributes)=104 while sizeof(OpcUa_VariableAttributes)=208. I also confirmed that the offsets of AccessLevel and UserAccessLevel in the larger structure were 0xb8 and 0xb9 respectively. That means any read of UserAccessLevel on a real OpcUa_NodeAttributes allocation would necessarily go past the end of the smaller object.
I intentionally set SpecifiedAttributes to 0 in the malformed request in order to characterize the first invalid read precisely. This matters because the sink function first checks whether the AccessLevel bit is set in SpecifiedAttributes. With SpecifiedAttributes equal to 0, the first conditional branch that could read AccessLevel is skipped. The next conditional contains a check on UserAccessLevel, and because the left side of the logical OR is false, C short-circuit evaluation continues into the right side and dereferences varAttributes->UserAccessLevel. That is the first actual out-of-bounds access in my reproduction.
The memory evidence matched the code path exactly. The allocation range reported by AddressSanitizer for the decoded attribute object was [0x50b000025b30, 0x50b000025b98), which is 104 bytes long. The invalid read occurred at 0x50b000025be9. This is 0x51, or 81 bytes, past the end of the allocated object, which is consistent with the expected UserAccessLevel offset in the larger OpcUa_VariableAttributes layout. The key crash signature was:
ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 1
#0 SOPC_NodeMgtHelperInternal_AddVariableNodeAttributes
#1 AddSingleVariableNode
#2 SOPC_AddressSpaceAccess_AddVariableNode
#3 address_space_bs__addNode_AddressSpace_Variable
#4 address_space__addNode_AddressSpace
#5 service_add_nodes_1__treat_add_nodes_item
This behavior demonstrates a real server-side heap out-of-bounds read caused by type confusion in protocol request processing. The most reliable security impact I confirmed is remote denial of service. An unauthenticated attacker is not automatically implied in every deployment because the reachability of AddNodes depends on how the server is built and how user authorization is configured, but any remote client that is permitted to invoke AddNodes against a server compiled with S2OPC_NODE_MANAGEMENT can trigger the bug with a single crafted request. I did not claim remote code execution. The verified and reproducible effect is server crash due to an invalid heap read during AddNodes handling.
My reproduction procedure was straightforward. I built S2OPC 1.7.3 with AddressSanitizer and node management enabled, using a configuration equivalent to:
cmake -S . -B build-asan-node -DCMAKE_BUILD_TYPE=RelWithDebInfo -DS2OPC_CLIENTSERVER_ONLY=ON -DENABLE_TESTING=ON -DENABLE_SAMPLES=ON -DWITH_ASAN=ON -DWARNINGS_AS_ERRORS=OFF -DS2OPC_NODE_MANAGEMENT=ON
Then I built the official validation server and my minimal PoC client target. I launched build-asan-node/bin/toolkit_test_server from the bin directory with the expected test key password environment variable. The server started normally and reported that the address space was configured and the demo server had started. I then launched my client PoC, which sent a malformed AddNodes request for a Variable node with a generic OpcUa_NodeAttributes payload. The client output showed that the request had been sent and that the synchronous service failed, while the server was terminated by ASan with a heap-buffer-overflow report.
In summary, this is a server-side type confusion vulnerability in S2OPC 1.7.3 AddNodes processing. The implementation accepts a generic OpcUa_NodeAttributes object in a Variable-node context, then unsafely downcasts it to OpcUa_VariableAttributes* and accesses Variable-specific fields on a smaller heap allocation. Because the two structures only share a safe common prefix, the code survives initial common-attribute handling but fails as soon as it dereferences Variable-only members. The bug is remotely reachable through a crafted AddNodesRequest when node management is enabled, and it causes a reproducible server crash.
A robust fix should enforce strict correspondence between NodeClass and the concrete NodeAttributes type before the request enters any specialized node-creation path. In practice, the validation logic in address_space_bs__addNode_check_valid_node_attributes_type should reject generic OpcUa_NodeAttributes for Variable nodes and only accept OpcUa_VariableAttributes. The same principle should be applied consistently to other specialized node classes as well. As a defense-in-depth measure, the Variable-specific AddNodes path should also verify at the point of use that Body.Object.ObjType is exactly OpcUa_VariableAttributes_EncodeableType before casting Body.Object.Value to const OpcUa_VariableAttributes*. Rejecting the malformed request early with a service error is preferable to attempting to reinterpret the generic object. |
|---|
| 원천 | ⚠️ https://gitlab.com/systerel/S2OPC/-/work_items/1787 |
|---|
| 사용자 | Carnegie (UID 98671) |
|---|
| 제출 | 2026. 07. 06. PM 02:45 (2 개월 ago) |
|---|
| 모더레이션 | 2026. 08. 22. AM 09:45 (2 months later) |
|---|
| 상태 | 수락 |
|---|
| VulDB 항목 | 394290 [Systerel S2OPC 까지 1.7.3 AddNodes Service sopc_node_mgt_helper_internal.c UserAccessLevel 정보 공개] |
|---|
| 포인트들 | 20 |
|---|