|
|
Message-ID: <179338396200.54632.11748087002055003897@gmail.com>
Date: Fri, 30 Oct 2026 11:12:42 -0700
From: Abhinav Agarwal <abhinavagarwal1996@...il.com>
To: oss-security@...ts.openwall.com
Subject: Re: CVE-2026-102508: Apache PLC4X: OPC UA secure channel: integrity
bypass, unverifiable server certificate, and silent downgrade
Hi,
I reported the findings behind CVE-2026-102508 and CVE-2026-102509 [1][2]. This follow-up gives the mechanisms and triggers for both. Both are fixed in PLC4X 1.0.0; there is no fixed 0.13.x. PLC4X is a client library, so the attacker is a malicious or impersonated server, or someone on the network path.
CVE-2026-102508 (0.9.0 - 0.13.1)
--------------------------------
Two defects in the OPC UA asymmetric receive path (0.12.0 - 0.13.1).
Bug 1, AsymmetricEncryptionHandler.verify(): the branch sense is inverted.
signature.update(message);
if (signature.verify(signatureData)) { // missing negation
throw new IllegalArgumentException("Invalid signature");
}
Bug 2, BaseEncryptionHandler.decodeMessage(): the chunk is decrypted, then verified against the original on-the-wire length.
if (chunk.isEncrypted()) {
bodySize = decrypt(chunkBuffer, chunk, messageLength); // new size
}
if (chunk.isSigned()) {
verify(chunkBuffer, chunk, messageLength); // discarded
}
verify() takes signatureStart = messageLength - signatureSize. Basic256Sha256 uses RSA/ECB/OAEPWithSHA-1AndMGF1Padding, so with a 2048-bit key each 256-byte ciphertext block decrypts to at most 214 bytes (OAEP overhead 2*hLen + 2 = 42); the two-block OPN response I used shrank by 84 bytes. The slice read as the signature is the tail of the real signature followed by stale post-decryption bytes.
Composed: bug 2 normally hands verify() bytes that do not validate, and bug 1 treats that as success. Zeros, random bytes and a correctly computed signature are all accepted; only a signature crafted to validate over the misaligned range is rejected. Fixing the negation alone would reject legitimate OPN responses.
The bypass covers OPN only: MSG/CLO chunks go through SymmetricEncryptionHandler, whose HMAC check is fail-closed. On its own it gives an on-path attacker no channel keys, since those derive from a client nonce encrypted to the server certificate the client trusts. The certificate default closes that gap: with no trust-store-file, OpcuaDriverContext used a permissive verifier and took the server certificate from the unauthenticated GetEndpoints response (discovery=true is the default). That certificate encrypts the OPN request, client nonce included, and the user's password.
Downgrade: security-policy defaults to None, and from 0.12.0 SecureChannel skips an endpoint only when policy and mode both mismatch, so a weaker policy with a matching mode is admitted:
if (!policyMatch && !msgSecurityMatch) {
continue;
}
>>From 0.13.0 the admitted candidates are sorted by ascending securityLevel, so the weakest survivor is preferred.
0.9.0 - 0.11.0 fail differently: EncryptionHandler logs a failed check at INFO and continues; checkSignature() digests the buffer including the signature, then reads the expected signature from the start of the buffer rather than the end; and there is no trust-anchor mechanism.
Reconstruction (Basic256Sha256, SignAndEncrypt, discovery=true, no trust store): a relay answers GetEndpoints with its own certificate; decrypts the client's OPN request and reads the client nonce; answers OPN with its own nonce, encrypted to the client certificate, under a signature that need not validate; derives the symmetric keys; then reads and authors correctly MACed MSG traffic, including ActivateSession with the password encrypted to the relay. In my testing (2048-bit keys) this captured the test credentials and changed a write from 100.0 to 999.0, confirmed on the real server by an independent observer, with no error at the client. The client was pointed directly at the relay; the relay's upstream leg used SecurityPolicy.None.
CVE-2026-102509 (0.10.0 - 0.13.1)
---------------------------------
Three defects reached during secure-channel and session setup, before user authentication. The CVE also covers two defects found during remediation: collections pre-sized from server-supplied element counts, and recursive mspec types with no depth limit (Go counterpart: CVE-2026-102510).
1. Byte strings (0.10.0 - 0.13.1). PascalByteString.staticParse() reads a signed int32 length, and ReadBufferByteBased.readByteArray() runs "new byte[numberOfBytes]" before checking the bytes exist. Trigger: after HEL/ACK and the client's OPN, answer with an 88-byte OPNF: the 12-byte message header (OPN, F, 4-byte size, 4-byte channel id), then securityPolicyUri (PascalString), senderCertificate and receiverCertificateThumbprint (PascalByteString). Set senderCertificate.stringLength to FF FF FF 7F: bytes 63-66 with the 47-byte SecurityPolicy#None URI, or from byte 73 with Basic256Sha256. This header is parsed before any certificate or signature check.
2. Array counts (0.10.0 - 0.13.1). FieldReaderArray.readFieldCount() bounds the wire count only against Integer.MAX_VALUE, then calls "new ArrayList<>(itemCount)", allocating the backing Object[] before any element is read. Exactly 0x7FFFFFFF fails fast ("Requested array size exceeds VM limit"); somewhat smaller counts attempt a multi-gigabyte allocation. Parser-level trigger: the Variant prefix 86 FF FF FF 7F (Int32 with the array bit set, then the count). On the wire: noOfServerEndpoints and noOfServerSoftwareCertificates in CreateSessionResponse, parsed in SecureChannel before ActivateSession.
3. Chunk accumulation (0.12.0 - 0.13.1). Conversation negotiates maxChunkCount and maxMessageSize but never checks them on receive. Trigger: after OPN and the client's CreateSessionRequest, stream MSG chunks flagged 'C' (continue) with the expected channel and token ids and increasing sequence numbers, never sending 'F' (final). I used 2,000 chunks of 61,440 bytes, roughly 120 MB.
Triggers 2 and 3 pass per-chunk symmetric decoding: under the default None policy they need no keys; under a secure policy the sender must be a server the client accepted. Cases 1 and 3 were driven over a socket, case 2 at parser level with its network route traced in source. The effect is loss of the client's OPC UA function: case 1 throws OutOfMemoryError repeatedly on the Netty thread while the client retries; case 3 exits the JVM only with -XX:+ExitOnOutOfMemoryError.
The array reader is in the shared plc4j-spi. OPC UA and ADS use 32-bit counts and lengths; Modbus and S7 use 8- or 16-bit ones.
Fixes
-----
- a2dbb6bfc0 (2026-07-10): negation restored, post-decrypt length passed to verify(), certificate verifier fails closed by default.
- 5a4d5bdb4c (2026-07-10): chunk count and message size limits enforced; array initial capacity capped at 1024.
- 372501287d (2026-06-12): SPI rewrite; the read buffer checks availability before allocating. Predates the report but first shipped in 1.0.0.
1.0.0 also defaults to Basic256Sha256 with SignAndEncrypt, rejects an unverifiable server certificate unless tls.verify=false, and fails the connection if the negotiated policy is weaker than configured. Until upgrading, a trust-store-file blocks the impersonating certificate (nothing was captured in my testing) but leaves the OPN check broken and does not stop trigger 1.
Downstream
----------
Almost every PLC4J consumer I checked pins a pre-1.0.0 release. CVE-2026-102508 needs plc4j-driver-opcua, which plc4j-driver-all includes.
- Apache Camel camel-plc4x: 0.13.1 in 4.23.0 (latest) and LTS 4.22.2 and 4.18.5; 0.13.0 in 4.14.9 (LTS); 0.12.0 in 4.10.0 and 4.8.0. Bundles the OPC UA driver, so both CVEs; the Spring Boot starter and Karaf feature inherit it. Camel main moved to 1.0.0 (CAMEL-25617, 2026-10-27) but has not released it.
- PLC4X's own integrations on the old org.apache.plc4x coordinates (NiFi NARs, Kafka Connect, Calcite, plc4x-opcua-server): 0.13.1 via plc4j-driver-all, so both CVEs. They are republished at 1.0.0 under org.apache.plc4x.extras.
- Apache StreamPipes 0.98.0: 0.13.1, plc4j-driver-all with opcua excluded (its OPC UA adapter uses Eclipse Milo). SPI defects only, with ADS among the shipped drivers.
- Apache HertzBeat 1.8.0 and 1.9.0: 0.12.0, Modbus and S7 drivers only. SPI defects only, bounded by those protocols' field widths.
Roughly a dozen smaller OT gateways and PLC bridges pin 0.9.0 - 0.13.1; json-scada is already on 1.0.0. openHAB, NiFi, IoTDB, Ditto and Node-RED do not depend on PLC4X.
Against a Camel route reading an OPC UA endpoint, a rogue or impersonated server can harvest the route's OPC UA credentials and alter the values it carries, so falsified data reaches the historian, database or topic downstream with no error at the Camel end.
Full write-up: https://abhinavagarwal07.github.io/posts/apache-plc4x-opcua-signature-bypass-preauth-dos/
Timeline: reported to security@...che.org 2026-07-09; routed to the PLC4X team and fixes committed to develop 2026-07-10; CERT/CC case VU#108987 opened 2026-07-25; 1.0.0 released 2026-09-07; CVEs published 2026-09-30.
[1] https://www.openwall.com/lists/oss-security/2026/09/30/4
[2] https://www.openwall.com/lists/oss-security/2026/09/30/5
Regards,
Abhinav Agarwal
Powered by blists - more mailing lists
Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.
Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.