PermissionType Browse permits reading every attribute except Value and RolePermissions. Direct Read requests previously bypassed allowBrowseNode for common metadata such as UserAccessLevel and UserExecutable.
Apply the Browse check to common attribute reads while retaining the dedicated permission handling for Value and RolePermissions.
RegisterServer and RegisterServer2 accepted an unbounded number of discovery URLs. With the MDNSD backend, every unique URL can enqueue additional probing records. A sufficiently large registration could therefore overflow the stack-backed mDNS output packet and terminate the Local Discovery Server.
Reject registrations containing more than 16 discovery URLs before any mDNS or registered-server state is modified.
Internal Vulnerability Advisory: open62541-SA-2026-0046
Require serialized PKCS#11 handles and the specified four-byte key nonce before copying key material into the TPM channel context. This rejects incompatible raw SKS keys and prevents out-of-bounds reads from undersized inputs.
Validate that the target of each PubSub information-model Remove method belongs directly to the object on which the method was invoked. This prevents a caller from using one authorized object to remove resources under another object.
processOPNResponse() fully decodes the OpenSecureChannelResponse
before checking whether the server replayed the SecureChannel's
current remote nonce. When that check rejects the response, the
function calls closeSecureChannel() and returns without freeing the
already-decoded response, leaking its ResponseHeader, SecurityToken
and ServerNonce fields. A server that keeps replaying nonces at a
reconnecting client grows the client's heap usage without bound, one
rejected OpenSecureChannelResponse at a time.
Free the decoded response before returning on this path.
Internal Vulnerability Advisory: open62541-SA-2026-0039
Co-Authored-By: Luigi Colluto <gigioneggiando@users.noreply.github.com>
Add check_hmac_verify_constanttime, exercising the OpenSSL
HMAC-SHA1/SHA256 verify helpers directly. A correct signature must
verify, a same-length signature with one flipped bit must be
rejected (guards the constant-time comparison), and a signature
shorter than the expected digest must be rejected without reading
past its end (guards the length check). The AddressSanitizer job in
CI will catch a regression of the latter.
Internal Vulnerability Advisory: open62541-SA-2026-0038
The OpenSSL HMAC-SHA1/SHA256 verify helpers compared the recomputed
MAC against the peer-supplied signature with UA_ByteString_equal(),
which reduces to a plain memcmp(). memcmp() carries no constant-time
guarantee and, on a byte-at-a-time implementation (the norm on the
small-footprint libcs open62541 is routinely built against), returns
as soon as it finds a differing byte -- a timing side channel a
remote peer can use to recover a valid MAC byte-by-byte.
Switch to UA_constantTimeEqual(), already used correctly by every
other symmetric SecurityPolicy (mbedTLS Basic128Rsa15, Basic256,
Basic256Sha256, Aes128Sha256RsaOaep). Since it takes a single length
and reads that many bytes from both buffers, first validate
signature->length against the expected MAC length, mirroring the
pattern used by the project's mbedTLS RSA policy Verify functions.
The digest length is fixed by the algorithm and policy, not attacker
data, so this early-return introduces no new timing side channel.
Internal Vulnerability Advisory: open62541-SA-2026-0038
Co-authored-by: Luigi Colluto <gigioneggiando@users.noreply.github.com>
UA_Response asyncResponse is declared without an initializer in
processAsyncResponse. When the body decode fails the code jumps to the
process label, which writes a single field (serviceResult) and then
invokes the user callback and UA_clear with a mostly-garbage object.
UA_clear walks every field according to the type descriptor and treats
uninitialized pointer-shaped fields as heap allocations, which may
cause invalid-free / memory corruption.
Initialize the response immediately after declaration so that every
execution path through the function operates on a well-defined object.
Co-Authored-By: Luigi Colluto <gigioneggiando@users.noreply.github.com>
Internal Vulnerability Advisory: open62541-SA-2026-0037
encrypt_sp_pubsub_aes128ctr_tpm/decrypt_sp_pubsub_aes128ctr_tpm and their
AES-256-CTR counterparts computed the block-aligned processing size
(sizeToEncrypt/sizeToDecrypt) by rounding data->length up to the next
16-byte boundary, but stored the result in a CK_BYTE. Two distinct bugs
followed from this:
- For a data->length whose rounded-up value exceeds 255, the 8-bit value
wraps around, so far fewer bytes than intended are processed while the
function still returns UA_STATUSCODE_GOOD.
- For any data->length that is not itself a multiple of 16, the rounded-up
size is larger than the caller's actual buffer. The PKCS#11 update calls
then read block-sized chunks directly from data->data past its end, and
the final loop that copies the result back writes that same rounded-up
size into data->data, overflowing the caller's allocation.
AES-CTR is a stream cipher and never needs block-aligned input. Fix this
by processing exactly data->length bytes throughout, using size_t/CK_ULONG
for all lengths instead of CK_BYTE: each PKCS#11 update call is chunked at
MAX_ENCRYPTION_SIZE bytes with a shorter final chunk when data->length is
not itself a multiple of it, operating on the caller's buffer directly
with no rounding, no padding, and no risk of the output exceeding the
caller's allocation.
Co-authored-by: Luigi Colluto <gigioneggiando@users.noreply.github.com>
Internal Vulnerability Advisory: open62541-SA-2026-0033
In readSubscriptionDiagnostics() and readSessionDiagnostics(), the
BrowseName of the target node was copied into a fixed-size 128-byte
stack buffer (memberName) without validating the length first. An
authenticated client with NodeManagement access could inject a variable
with a BrowseName longer than 127 bytes under
SessionDiagnosticsObjectType with a HasModellingRule->Mandatory
reference. On the next session creation the server would instantiate
that member into the session diagnostics object and attach the
diagnostics read callback. A subsequent Read on that variable triggers
a stack-buffer overflow in memcpy(), writing attacker-controlled data
beyond the memberName buffer into adjacent stack variables, causing
remote denial of service and potential memory corruption.
Fix: add an explicit length guard before memcpy() in both callbacks.
If the BrowseName length is equal to or greater than sizeof(memberName),
return UA_STATUSCODE_BADNODEIDUNKNOWN and skip the copy entirely.
The same pattern exists in readSubscriptionDiagnostics() for the
SubscriptionDiagnosticsDataType member lookup and in
readSessionDiagnostics() for the SessionDiagnosticsDataType /
SessionSecurityDiagnosticsDataType member lookup.
Internal Vulnerability Advisory: open62541-SA-2026-0023
Co-authored-by: Huashuo <MrAlaskan@users.noreply.github.com>
UA_DataValue_backend_copyRange() performed a shallow memcpy of the
source UA_DataValue before attempting the range copy. Because the
destination's UA_Variant fields were not reinitialized first, a
failing UA_Variant_copyRange() (for example when the client-supplied
indexRange exceeds UA_MAX_ARRAY_DIMS) left the destination value as an
uninitialized alias of the backend-owned storage instead of an
independent copy.
copyDataValues_backend_memory() also discarded the status code from
both UA_DataValue_backend_copyRange() and UA_DataValue_copy(), so the
aliased, partially-copied value was still counted as a successful
result and forwarded to the HistoryRead response. Freeing that
response later released the backend's live storage, leaving the
backend with a dangling pointer that was dereferenced on a subsequent
HistoryRead.
Reinitialize the destination variant before the range copy and clear
it again on failure, mirroring the safe pattern already used by
UA_DataValue_copyRange(). Propagate copy failures out of
copyDataValues_backend_memory() immediately instead of counting a
failed entry as provided.
Internal Vulnerability Advisory: open62541-SA-2026-0021
Co-authored-by: gff-cw <gff-cw@users.noreply.github.com>
Require a canonical Good service result and exactly one operation result before synchronous attribute and history helpers access the first result.
Return noncanonical Good, Uncertain and Bad top-level status codes immediately. This avoids indexing an empty result array when a peer sends such a service result without operation results.
Count matching records before allocating the response and fill it in a second pass. This removes the stack array sized from the unbounded multicast discovery cache while preserving result ordering and limits.
Add a regression that serves 50,000 cached records from a 128 KiB worker stack.
The asymmetric security header includes the local certificate and must fit into the peer-negotiated send buffer. Computing payload pointers before checking that relationship could underflow the remaining capacity and move the encoder bounds outside the allocated chunk.
Validate the header, signature, encryption-block and padding requirements as lengths first. Return BadEncodingLimitsExceeded when the negotiated chunk cannot carry them, and cover the oversized, incomplete-block and exact-boundary cases.
Compare the declared body length with the remaining input before computing the
end pointer. This avoids undefined pointer arithmetic for oversized lengths.
Correct the bounds checks introduced by 57142d6b4.
Reject oversized discoveryUrl paths before they reach the earlier newUrl VLA or modify discovery and mDNS state.
Fixes: 736102e8b (fix(discovery): cap discoveryUrl path length to prevent VLA stack exhaustion)
Mark allocated ExtensionObject content as decoded before validating the body
length. This lets normal decoder error cleanup release the allocation when
length validation or content decoding fails.
Fixes: 57142d6b4 (fix(core): validate ExtensionObject body length against decoded byte count)
The binary decoder for ExtensionObject and Variant-of-ExtensionObject
skipped the on-wire body-length field without verifying that the inner
type decoder consumed exactly that many bytes. A crafted message could
cause the decoder's byte-position to diverge from the length-field's
authoritative interpretation (OPC UA Part 6 §5.2.2.15), creating a
decoder-vs-IDS split-view: any inline network monitor parsing per the
spec observes a different logical message than the server processes.
Two call sites are fixed (Variant-of-EO-array was not yet present in 1.3):
1. ExtensionObject_decodeBinaryContent – pre-auth reachable via
ActivateSessionRequest.UserIdentityToken.
2. Variant_decodeBinaryUnwrapExtensionObject – pre-auth reachable via
Variants in OpenSecureChannelRequest / ActivateSessionRequest.
At every site, the body length is now read into a local variable, an
expected-end pointer is computed, and a UA_STATUSCODE_BADDECODINGERROR
is returned if the inner decoder leaves the position anywhere other than
expected_end.
See: open62541-SA-2026-0018
Thanks to Chuan Wei [SCU] for the responsible disclosure.
An attacker-controlled discoveryUrl.path component was propagated
without any upper-bound check into a C99 variable-length array inside
UA_Discovery_addRecord. On platforms where UA_STACKARRAY expands to a
true VLA, a RegisterServer request carrying an oversized path causes
the server thread's stack to be exhausted, crashing the process.
pathChars is changed from an attacker-sized stack VLA to a fixed
1024-byte stack buffer, with a bounds check placed directly at its
declaration. The 1024-byte bound exceeds any legitimate mDNS
TXT-record path in practice and is consistent with the 63-char DNS
label limit already enforced for hostname and servername.
Internal Vulnerability Advisory: open62541-SA-2026-0020
Thanks to Chuan Wei [SCU] for the responsible disclosure.
In getHistoryData_service_default(), the outer continuation point encodes
a skip count that was read directly from client-controlled bytes without
validation. Two independent arithmetic errors could cause a heap-buffer-
overflow in the subsequent backend copy call:
1. When skip > _resultSize, the subtraction `_resultSize - skip` wraps
around in size_t, producing a very large value. After clamping to
maxSize, the allocation is correct, but the wrapped large skip is
still used to recompute valueSize for the backend call.
2. When the reconstructed valueSize exceeds the number of entries actually
allocated in outResult, copyDataValues() writes beyond the end of the
result array (heap-buffer-overflow write).
Fix both issues in getHistoryData_service_default():
- Reject continuation points whose skip value exceeds the computed result
set size, returning UA_STATUSCODE_BADCONTINUATIONPOINTINVALID.
- After computing valueSize, cap it to the remaining allocated capacity in
outResult so the backend cannot be instructed to write past the buffer.
Internal Vulnerability Advisory: open62541-SA-2026-0027
__Client_readAttribute() had a NodeClass special case copying a fixed
4 bytes from res->value.data without validation. A Boolean (memSize 1)
response causes a heap-buffer-overflow (GHSA-q3qv-2x6p-gfmp, CWE-125).
Fix: keep the special case (NodeClass is an enum, encoded as Int32, so
the generic type-pointer comparison won't match) but require a scalar
variant with non-NULL data and a type whose memSize is exactly
sizeof(UA_NodeClass).
The Call service resolves a method's expected argument types from the
InputArguments property reachable from the MethodNode at call time.
This metadata lives in the regular address space and can be replaced
through standard NodeManagement services.
The built-in Alarms & Conditions Acknowledge, AddComment and Confirm
method callbacks assumed the fixed standard argument signature
(ByteString EventId, LocalizedText Comment) and cast the incoming
Variant payloads accordingly without checking their actual runtime
type. If the InputArguments metadata for one of these methods is
replaced with a different argument signature, a Call with
correspondingly-typed arguments passes the generic service-layer
validation but is then reinterpreted by the callback under the
original, incompatible type, leading to a type confusion and an
out-of-bounds heap read that crashes the server.
Add an explicit runtime check of the argument count and scalar types
before the EventId and Comment arguments are dereferenced in any of
the three callbacks.
Internal Vulnerability Advisory: open62541-SA-2026-0024
Operation_TransferSubscription() copies the UA_Subscription struct with
memcpy but only migrates monitoredItems, notificationQueue, and
retransmissionQueue. The samplingMonitoredItems list was not re-initialized
in the new subscription, leaving stale le_prev backpointers from items
with samplingType==PUBLISH pointing into the freed old subscription struct.
When UA_MonitoredItem_unregisterSampling() calls LIST_REMOVE, it writes
to freed memory (heap-use-after-free).
Add migration of samplingMonitoredItems to the new subscription after
the point of no return, matching the pattern used for monitoredItems.
Internal Vulnerability Advisory: open62541-SA-2026-0015
Co-authored-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com>
Add bounds check in verifyAndDecrypt() to prevent unsigned integer
underflow when buffer->length is smaller than the expected signature
size. Without this check, the subtraction wraps around producing a
large size_t that causes mbedtls to read unmapped memory.
Internal Vulnerability Advisory: open62541-SA-2026-0014
Co-authored-by: Abhinav Agarwal <abhinavagarwal1996@gmail.com>
Multiplying attacker-controlled arrayDimensions values into a size_t
accumulator without an overflow check allows the product to wrap and
match a small arrayLength. Subsequent ranged read or write operations
then access memory outside the allocated array bounds.
Add checked multiplication at all sites where the dimension product is
computed: Variant binary decode, Variant binary encode, checkAdjustRange,
and computeStrides (as a defence-in-depth measure).
Internal Vulnerability Advisory: open62541-SA-2026-0012 and open62541-SA-2026-0013
Co-authored-by: Asher Davila <AsherDLL@users.noreply.github.com>
Co-authored-by: Malav Vyas <MalavVyas@users.noreply.github.com>
The default server configuration zero-initialises
maxPublishReqPerSession, which disables the per-session cap on
outstanding PublishRequest entries. A remote client that is allowed
to create a session and a subscription can therefore send an
unbounded number of PublishRequest messages, each of which allocates
a UA_PublishResponseEntry and appends it to the session's publish
queue. The resulting monotonic heap growth causes process memory
exhaustion and a denial of service.
Set a safe finite default of 512 outstanding PublishRequests per
session in setDefaultConfig() so that the cap is enforced out of the
box, while still allowing deployers to override it.
Internal Vulnerability Advisory: open62541-SA-2026-0010
Co-Authored-By: keyme <kmm2003@users.noreply.github.com>
Variant_setRange uses sizeof(elem_size) instead of elem_size in
the memset that zeros moved array elements. sizeof(elem_size)
always evaluates to sizeof(size_t), which is 8 on 64-bit. For
types with memSize > 8 (e.g. UA_String at 16 bytes), only half
the struct is zeroed, leaving heap pointers dangling. This causes
a double-free when both the source and the variant are cleared.
The JSON decoder for PubSub DataSetPayload casts the token count to UA_UInt16
without bounds checking. If a malicious or malformed message provides more than
UA_UINT16_MAX/2 tokens, the cast silently truncates to a smaller value while the
actual allocation uses the full (large) length. This can lead to heap buffer
overflows and memory corruption.
Add an explicit bounds check before the cast, rejecting messages with a field
count that exceeds UA_UINT16_MAX.
See: open62541-SA-2026-0001
Rearrange the overflow check in Array_decodeBinary so that the
multiplication `type->memSize * length` cannot overflow before the
comparison. Instead of checking
ctx->pos + (type->memSize * length) / 128 <= ctx->end
which overflows when the product exceeds SIZE_MAX, rewrite it as
length / 128 <= remaining / type->memSize
where `remaining = ctx->end - ctx->pos`, keeping all operations in safe
size_t range.
Internal Vulnerability Advisory: open62541-SA-2026-0003
The TranslateBrowsePathsToNodeIds service did not call the allowBrowseNode
access-control callback when resolving path elements or terminal targets.
A client denied direct Browse access to a node could still use that node as a
waypoint or receive its NodeId as a path translation result, disclosing hidden
nodes and their children.
Add the same allowBrowseNode gate already applied in the Browse service to both
walkBrowsePathElement (intermediate nodes) and
Operation_TranslateBrowsePathToNodeIds (terminal nodes).
Internal Vulnerability Advisory: open62541-SA-2026-0004
Co-authored-by: keyme <kmm2003@users.noreply.github.com>