| Beschreibung | I discovered a server-side memory-safety vulnerability in S2OPC 1.7.3 (commit b4c5c7d63cd69698461d514b905a7c92b3c377c4) while testing the official validation server toolkit_test_server through the normal OPC UA network write path. The issue is a heap out-of-bounds read in the handling of WriteRequest messages that target a String[] node together with a two-dimensional IndexRange. In my validation, the vulnerable target node was ns=1;s=Array_String_001 and the crafted write used IndexRange "0:2,0" with a source variant containing only one string element. This reaches the server as a network request, is decoded by the normal message decoder, and then crashes during the server-side write operation before a normal WriteResponse is returned.
The vulnerability exists because the String[] matrix/range write logic derives the number of loop iterations from the destination range span, but it does not verify that the source array actually contains that many elements. In the vulnerable path, the request flows through service_write__treat_write_request(), address_space_bs__set_Value(), set_value_indexed_helper(), SOPC_Variant_SetRange(), set_range_matrix(), set_range_matrix_on_string_array(), and finally set_range_string(). The key issue is in set_range_matrix_on_string_array() inside src/Common/opcua_types/sopc_builtintypes.c. That function computes the number of strings to update from the first IndexRange dimension, effectively using the destination range length as the copy count. It then iterates over that count and accesses src->Value.Array.Content.StringArr[i] for each iteration. However, there is no validation ensuring that src->Value.Array.Length is at least as large as the destination range span. As a result, if the first dimension covers three strings but the attacker only provides one source string, the second iteration already reads beyond the end of the decoded source array.
A simplified description of the failing logic is as follows. The first IndexRange dimension 0:2 is interpreted as a span of three elements, so the code computes array_length = end - start + 1 = 3. It then executes a loop from i = 0 to i < array_length and passes &src->Value.Array.Content.StringArr[i] into set_range_string(). In my proof-of-concept request, the source variant has Value.Array.Length = 1, so only StringArr[0] is valid. When i becomes 1, the server dereferences StringArr[1], which is already outside the allocated heap region that was created when the variant array was decoded from the network message. The subsequent call into set_range_string() accesses fields of a nonexistent SOPC_String object, leading AddressSanitizer to report a heap-buffer-overflow read.
This is not a synthetic parser-only test or an internal unit-test misuse. I reproduced it against the official toolkit_test_server binary shipped in the S2OPC validation tests, using a dedicated network client that established a normal secure connection and then sent a WriteRequest to the server. The client successfully connected and issued the write to the target node. On the client side, the synchronous service call failed with status 8 and no response object, which is consistent with the server aborting before returning a valid service response. On the server side, AddressSanitizer reported a heap-buffer-overflow read in set_range_string() with the stack trace showing set_range_matrix_on_string_array(), set_range_matrix(), SOPC_Variant_SetRange(), set_value_indexed_helper(), address_space_bs__set_Value(), service_write__treat_write_request(), and the normal request-processing path above it. The ASan report also showed that the invalid read happened immediately past a 16-byte heap region allocated during message decoding in SOPC_Read_Array(), which is consistent with a decoded one-element SOPC_String array being accessed as if it contained more elements than were actually provided by the client.
The confirmed security impact is remote denial of service against a reachable S2OPC server instance. A client that is able to submit a write to a compatible String[] node can trigger a memory-safety violation and crash the server process. In an AddressSanitizer build this is an immediate abort. In a non-sanitized production build, the behavior depends on allocator layout and runtime conditions, but the operation is still undefined and can reasonably result in process termination or other unstable behavior. At this stage, my validation supports a reliable crash/DoS claim. I am not claiming reliable code execution, and I do not think the current evidence is sufficient to present this as RCE. The correct classification is a server-side out-of-bounds read leading to denial of service.
The weakness is best described as CWE-125: Out-of-bounds Read. The bug is in application-level bounds validation rather than transport parsing alone. The server accepts a structurally valid WriteRequest, decodes the source array according to the element count actually present on the wire, but later uses the destination range span as the effective source length. This creates a semantic length mismatch between the attacker-controlled IndexRange and the attacker-controlled source array length. Because the loop trusts the range-derived count, the memory access escapes the bounds of the decoded StringArr allocation.
The issue appears in S2OPC 1.7.3, commit b4c5c7d63cd69698461d514b905a7c92b3c377c4, and is located in the String[] range-writing logic in src/Common/opcua_types/sopc_builtintypes.c, with reachability through src/ClientServer/services/b2c/address_space_bs.c and the standard write service manager path. The allocator evidence from the crash shows that the source array is allocated during message decoding in src/Common/opcua_types/sopc_encoder.c, after which the later write helper incorrectly reads beyond that decoded allocation.
My reproduction steps were straightforward. First, I built S2OPC 1.7.3 with AddressSanitizer enabled. Second, I launched the official toolkit_test_server validation server. Third, I used a custom client to connect with the normal client configuration and send a WriteRequest targeting ns=1;s=Array_String_001 with IndexRange "0:2,0". The variant payload was a one-element String array containing only "Z". This request is important because the first dimension instructs the server to update three array entries, while the provided source array contains only one element. The malformed cardinality is not rejected before the copy loop begins. The client output showed successful configuration loading and connection establishment, then the write attempt, followed by a service failure without a response. The server output showed a live session creation and activation, then an AddressSanitizer heap-buffer-overflow report at set_range_string() on a read of size 4, with the invalid address located exactly at the end of a small heap allocation.
The root cause can be fixed cleanly with an explicit cardinality check before the loop in set_range_matrix_on_string_array(). After computing the number of required source elements from the first range dimension, the code should verify that src->ArrayType is Array, src->BuiltInTypeId is SOPC_String_Id, the range is well-formed, and critically that src->Value.Array.Length is greater than or equal to the computed array_length. If this validation fails, the server should reject the write with a normal service-level validation error such as Bad_IndexRangeInvalid or another consistent status already used by this path. This prevents the loop from ever dereferencing src->Value.Array.Content.StringArr[i] beyond the decoded source allocation.
From a vulnerability-management perspective, this issue is notable because it is reachable through a real server deployment path rather than through a contrived helper invocation. The attack requires network access to the OPC UA service endpoint and permission to perform a write against a matching String[] node, but no local access or special instrumentation is required to trigger the underlying bug. Because S2OPC is used as an OPC UA toolkit and validation platform, this kind of input-driven server crash has clear operational relevance for integrators who expose writable array nodes to clients.
In summary, I discovered and reproduced a genuine server-side heap out-of-bounds read in S2OPC 1.7.3. A remote client can send a crafted WriteRequest with a two-dimensional IndexRange whose destination span is larger than the supplied source String[] length. The implementation trusts the range-derived span and iterates past the end of the decoded source array, causing an out-of-bounds read in set_range_string() and crashing the server. The confirmed impact is remote denial of service. The fix is to validate source array cardinality against the requested range span before entering the copy loop. |
|---|