8399 Commits

Author SHA1 Message Date
Niels Beier 711011df7c refactor(deps): warn that MQTT-C is deprecated
MQTT-C is barely maintained and has unfixed issues.
Point to the libwebsockets MQTT implementation on master.
2026-10-09 15:25:17 +02:00
o6-jpfr 15dcadd012 refactor(build): Bump version to v1.3.22 v1.3.22 2026-10-04 20:17:28 +02:00
StalderT 33946dfb57 fix(server): require Browse for common attribute reads
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.
2026-09-18 23:24:29 +02:00
Julius Pfrommer 15b09d45da fix(discovery): limit registered server discovery URLs
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
2026-09-18 13:52:16 +02:00
Julius Pfrommer cf77bddfd3 fix(pubsub): guard method-only reference helper 2026-09-18 12:14:58 +02:00
Julius Pfrommer 2f478172b4 fix(deps): use published mqtt-c revision
Keep the submodule at a revision available from its configured upstream so clean checkouts and CI can initialize it.
2026-09-18 12:14:58 +02:00
Julius Pfrommer f782a38cb6 fix(pubsub): validate TPM key handles and nonce
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.
2026-09-18 12:14:58 +02:00
Julius Pfrommer 25aa35db42 fix(pubsub): bound UDP interface-name stack use
Reject interface names that do not fit the transport's fixed input buffer and replace the request-sized stack allocation with a fixed-size array.
2026-09-18 12:14:58 +02:00
Julius Pfrommer aaaabd427c fix(pubsub): scope Remove methods to their object
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.
2026-09-18 12:14:58 +02:00
Julius Pfrommer 935588a18b fix(deps): update mqtt-c topic bounds validation
Update mqtt-c to reject PUBLISH packets whose topic length exceeds the MQTT Remaining Length. This prevents an out-of-bounds read while decoding malformed broker input.\n\nAddresses REPORT-03-2026-08-20 / VULN-17.
2026-09-18 12:14:58 +02:00
Niels Beier 246748fe54 fix(client): free the decoded OpenSecureChannelResponse on nonce reuse rejection
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>
2026-09-15 20:35:54 +02:00
Niels Beier a0d58fca9e test(crypto): add regression tests for HMAC verify constant-time fix
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
2026-09-15 20:31:48 +02:00
Niels Beier 1c1cbbdf8a fix(crypto): use constant-time, length-checked comparison for HMAC/MAC verification
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>
2026-09-15 20:31:48 +02:00
Niels Beier 304cf8f6b7 fix(client): initialize async response before decode
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
2026-09-15 20:31:19 +02:00
Niels Beier b6bd2cf88d fix(pubsub): fix OOB read/write and truncation in AES-CTR TPM en/decrypt
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
2026-09-15 20:10:23 +02:00
Niels Beier 26f2760a88 fix(server): bounds-check BrowseName length before copying into diagnostics memberName buffer
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>
2026-09-15 20:09:49 +02:00
Julius Pfrommer 9cedef34bc refactor(build): Bump version to v1.3.21 v1.3.21 2026-09-06 21:58:25 +02:00
Niels Beier 7a08139590 fix(historydata): prevent UAF in memory backend range-limited history reads
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>
2026-09-04 13:20:06 +02:00
Julius Pfrommer f80563604a fix(client): validate service result before reading result arrays
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.
2026-08-30 23:02:06 +02:00
Julius Pfrommer 697cdf07f4 fix(discovery): avoid cache-sized FindServersOnNetwork stack use
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.
2026-08-30 23:02:06 +02:00
Julius Pfrommer 27e07a0e73 fix(core): validate asymmetric OPN header bounds
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.
2026-08-30 18:08:18 +02:00
Julius Pfrommer 318b716b00 refactor(build): Bump version to v1.3.20 v1.3.20 2026-08-20 14:40:14 +02:00
Julius Pfrommer a68d6deeb7 fix(core): validate ExtensionObject length without pointer overflow
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.
2026-08-16 08:37:59 +02:00
Julius Pfrommer 150706770f fix(discovery): validate mDNS path before changing state
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)
2026-08-15 23:28:19 +02:00
Julius Pfrommer 9797082a01 fix(core): release ExtensionObject content after decode errors
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)
2026-08-15 23:28:19 +02:00
Julius Pfrommer 3c2d4509ee refactor(server): Limit the recursion depth to instantiate mandatory children
(cherry picked from commit 6e8af5dc44)
2026-08-14 10:12:35 +02:00
Niels Beier 57142d6b4c 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.
2026-08-14 10:12:35 +02:00
Niels Beier 736102e8b2 fix(discovery): cap discoveryUrl path length to prevent VLA stack exhaustion
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.
2026-08-14 10:12:35 +02:00
Niels Beier f828c6051d fix(historydata): validate and cap continuation point skip in HistoryRead
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
2026-08-14 10:12:35 +02:00
open62541 security d0a6f805f0 fix(client): Guard NodeClass read against type/size mismatch
__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).
2026-08-14 10:12:35 +02:00
Niels Beier 8b16097c2c fix(server): Validate EventId/Comment argument types A&C
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
2026-08-14 10:12:35 +02:00
Andreas Ebner 81b9c3a6e5 fix(ci): Use the open62541 CI images instead of a personal registry 2026-08-11 15:38:03 +02:00
Julius Pfrommer 00c485444e refactor(build): Bump version to v1.3.19 v1.3.19 2026-07-27 13:14:35 +02:00
Niels Beier 2da371bcda fix(server): migrate samplingMonitoredItems in TransferSubscriptions
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>
2026-07-23 21:19:43 +02:00
Niels Beier a1257feec0 fix(pubsub): check message length before signature size subtraction
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>
2026-07-23 20:37:36 +02:00
Niels Beier 8098907673 fix(core): reject overflowing arrayDimensions product in Variant en/decoding
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>
2026-07-23 20:37:00 +02:00
Niels Beier 9c1636a232 fix(server): Cap default outstanding PublishRequest count per session
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>
2026-07-09 14:10:23 +02:00
lonelybones 7cf493ecca fix(core): use correct size in memset after moving Variant array elements
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.
2026-07-01 11:40:58 +02:00
Julius Pfrommer db393f2c94 refactor(build): Bump version to v1.3.18 v1.3.18 2026-06-28 17:34:08 +02:00
Julius Pfrommer 435ac16aae fix(build): Fix FindCheck for cross-arch builds 2026-06-27 10:08:44 +02:00
Niels Beier 0eb7d97ada fix(pubsub): Guard DataSetPayload JSON field count against UA_UInt16 overflow
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
2026-06-25 19:54:28 +02:00
Tomer Dricker 9e0d23838e fix(core): Fix integer overflow in array decoding
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
2026-06-25 19:53:31 +02:00
Julius Pfrommer 683bb385dc fix(build): Ensure the check library is found on MacOS 2026-06-25 14:14:12 +02:00
Julius Pfrommer bdc9497368 fix(tests): Fix an endless loop on CI runs 2026-06-25 14:14:12 +02:00
Julius Pfrommer 6f58e2cf84 fix(client): Return the precise statuscode when triggering a reconnect 2026-06-25 14:14:12 +02:00
Niels Beier 20b9990c5e fix(server): Apply allowBrowseNode access control in TranslateBrowsePathsToNodeIds
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>
2026-06-25 14:14:12 +02:00
aaghash.r d6fb639f29 fix(client): update status codes for error masking 2026-03-09 22:41:12 +01:00
Julius Pfrommer 41f4deef34 refactor(build): Bump version to v1.3.17 v1.3.17 2026-02-02 21:06:41 +01:00
dpazj ff411f0734 fix(server): Avoid comparing a scalar value and a null-array when detecting deadband value changes 2026-01-23 20:02:59 +01:00
Marvin Gnad 51ff6593ec fix(plugin): fix namelist allocated from scandir not freed
(cherry picked from commit bcb02196fb)
2025-12-09 11:20:38 +01:00