This is an automated email from the ASF dual-hosted git repository.
chrisdutz pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git
The following commit(s) were added to refs/heads/develop by this push:
new 5a4d5bdb4c fix: Addessed various issues regarding to parsing of
potentially corrupt or mallicious responses which could cause OOM errors.
5a4d5bdb4c is described below
commit 5a4d5bdb4c1b0e9356eff00af198bdb6aa960f26
Author: Christofer Dutz <[email protected]>
AuthorDate: Fri Jul 10 14:01:04 2026 +0200
fix: Addessed various issues regarding to parsing of potentially corrupt or
mallicious responses which could cause OOM errors.
---
.../plc4x/java/ads/resolution/ValueDecoder.java | 19 ++++++++++++++++++-
.../canopen/conversation/SDOUploadConversation.java | 21 ++++++++++++++++++++-
.../apache/plc4x/java/opcua/OpcuaConnection.java | 9 +++++++++
.../plc4x/java/opcua/context/Conversation.java | 18 ++++++++++++++++++
.../java/opcua/protocol/chunk/ChunkStorage.java | 7 +++++++
.../opcua/protocol/chunk/MemoryChunkStorage.java | 5 +++++
.../spi/fields/fields/reader/FieldReaderArray.java | 14 +++++++++++++-
7 files changed, 90 insertions(+), 3 deletions(-)
diff --git
a/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/resolution/ValueDecoder.java
b/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/resolution/ValueDecoder.java
index 8931c8619a..10bda422de 100644
---
a/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/resolution/ValueDecoder.java
+++
b/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/resolution/ValueDecoder.java
@@ -48,6 +48,14 @@ import java.util.Map;
*/
public final class ValueDecoder {
+ /**
+ * Upper bound for the initial capacity of a list whose element count
comes from the
+ * (server-supplied) data type table. Prevents a bogus,
attacker-controlled array dimension
+ * from triggering an eager multi-GB allocation before any element is
actually read. The list
+ * still grows past this if that many elements really are present on the
wire.
+ */
+ private static final int MAX_INITIAL_CAPACITY = 1024;
+
private final Map<String, AdsDataTypeTableEntry> dataTypeTable;
public ValueDecoder(Map<String, AdsDataTypeTableEntry> dataTypeTable) {
@@ -98,7 +106,16 @@ public final class ValueDecoder {
AdsDataTypeArrayInfo cur = dims.get(0);
List<AdsDataTypeArrayInfo> rest = dims.subList(1, dims.size());
long count = cur.getNumElements();
- List<PlcValue> elements = new ArrayList<>((int) count);
+ // numElements is a wire-supplied unsigned 32-bit value (up to
0xFFFFFFFF). Reject counts
+ // that don't fit a positive int before the (int) cast - otherwise a
large value either
+ // overflows to a negative capacity (NegativeArraySizeException) or
forces an eager
+ // multi-GB allocation (OOM / DoS). The list itself is not pre-sized
to the untrusted
+ // count: it grows as elements are actually decoded, and
dataReader.read() throws once the
+ // buffer is exhausted.
+ if (count > Integer.MAX_VALUE) {
+ throw new BufferException("Array count of " + count + " exceeds
the maximum allowed count of " + Integer.MAX_VALUE);
+ }
+ List<PlcValue> elements = new ArrayList<>((int) Math.min(count,
MAX_INITIAL_CAPACITY));
for (long i = 0; i < count; i++) {
elements.add(decodePartialArray(rb, arrayDataType, rest));
}
diff --git
a/plc4j/drivers/canopen/src/main/java/org/apache/plc4x/java/canopen/conversation/SDOUploadConversation.java
b/plc4j/drivers/canopen/src/main/java/org/apache/plc4x/java/canopen/conversation/SDOUploadConversation.java
index 9fc4fdf8fe..e9ce200666 100644
---
a/plc4j/drivers/canopen/src/main/java/org/apache/plc4x/java/canopen/conversation/SDOUploadConversation.java
+++
b/plc4j/drivers/canopen/src/main/java/org/apache/plc4x/java/canopen/conversation/SDOUploadConversation.java
@@ -57,6 +57,15 @@ public class SDOUploadConversation {
private static final Logger LOGGER =
LoggerFactory.getLogger(SDOUploadConversation.class);
+ /**
+ * Upper bound for the buffer eagerly allocated for a segmented SDO
upload. The "total bytes"
+ * figure comes from the remote node's initiate-upload response (an
unsigned 32-bit wire
+ * field), so a bogus value could otherwise trigger a multi-GB allocation
before a single
+ * segment is read (OOM / DoS). SDO uploads carry object-dictionary values
fetched in ≤7-byte
+ * segments, so 16 MiB is already an extremely generous ceiling for any
legitimate transfer.
+ */
+ private static final long MAX_SEGMENTED_UPLOAD_SIZE = 16L * 1024 * 1024;
+
private final CANConversation conversation;
private final int nodeId;
private final int answerNodeId;
@@ -118,7 +127,17 @@ public class SDOUploadConversation {
completeWithValue(receiver, payload.getData(),
payload.getData().length);
} else if (answer.getPayload() instanceof
SDOInitiateSegmentedUploadResponse) {
SDOInitiateSegmentedUploadResponse segment =
(SDOInitiateSegmentedUploadResponse) answer.getPayload();
- int totalSize = (int) segment.getBytes();
+ long announcedSize = segment.getBytes();
+ // Don't eagerly allocate a buffer sized to the untrusted,
node-announced total: a bogus
+ // value (e.g. 0x40000000) would trigger a huge allocation before
any segment is read,
+ // and 0xFFFFFFFF would even cast to a negative array size. Reject
implausible sizes up
+ // front; the buffer is still filled incrementally from the
segments actually received.
+ if (announcedSize < 0 || announcedSize >
MAX_SEGMENTED_UPLOAD_SIZE) {
+ receiver.completeExceptionally(new PlcException(
+ "SDO segmented upload announced an implausible size of " +
announcedSize + " bytes"));
+ return;
+ }
+ int totalSize = (int) announcedSize;
fetch(receiver, new byte[totalSize], 0, totalSize, false);
} else {
receiver.completeExceptionally(new PlcException("Unsupported SDO
upload kind."));
diff --git
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
index bf4adc2f43..b24c3dbacf 100644
---
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
+++
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java
@@ -617,6 +617,15 @@ public class OpcuaConnection extends
ConnectionBase<OpcuaConfiguration> implemen
return new PlcList(flatValues);
}
int currentDim = dimensions.getFirst();
+ // The array dimensions are raw signed int32 values taken straight off
the wire, independent
+ // of how many values were actually sent. Only partition when the
declared dimension is
+ // consistent with the materialized value count: this bounds the work
to real received data
+ // and avoids both a division by zero (currentDim == 0) and an eager
multi-GB allocation /
+ // loop for an attacker-supplied dimension (e.g. 0x7fffffff).
Otherwise fall back to a flat
+ // list, matching the behaviour when no dimensions are declared.
+ if (currentDim <= 0 || currentDim > flatValues.size() ||
flatValues.size() % currentDim != 0) {
+ return new PlcList(flatValues);
+ }
List<Integer> remainingDims = dimensions.subList(1, dimensions.size());
int chunkSize = flatValues.size() / currentDim;
List<PlcValue> result = new ArrayList<>(currentDim);
diff --git
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/Conversation.java
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/Conversation.java
index 048fba6c4c..492880d48d 100644
---
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/Conversation.java
+++
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/Conversation.java
@@ -503,6 +503,24 @@ public class Conversation implements SecureChannelState {
}
storage.append(((BinaryPayload) data).getPayload());
+ // Enforce the negotiated receive limits while accumulating chunks.
Without this a malicious
+ // or faulty server could stream an unbounded number of CONTINUE
chunks (never sending a
+ // FINAL), growing the in-memory buffer without bound until the client
runs out of memory.
+ // A limit of 0 means "no limit" per the OPC UA spec, so only enforce
positive limits.
+ if (limits != null) {
+ long maxChunkCount = limits.getMaxChunkCount();
+ if (maxChunkCount > 0 && storage.count() > maxChunkCount) {
+ storage.reset();
+ throw new IllegalStateException("Received message exceeds the
negotiated maximum chunk count of " + maxChunkCount);
+ }
+ long maxMessageSize = limits.getMaxMessageSize();
+ if (maxMessageSize > 0 && storage.size() > maxMessageSize) {
+ storage.reset();
+ throw new IllegalStateException("Received message size " +
storage.size()
+ + " exceeds the negotiated maximum message size of " +
maxMessageSize + " bytes");
+ }
+ }
+
return FINAL.equals(chunkType);
}
diff --git
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkStorage.java
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkStorage.java
index a34e50fc25..e7e793ee28 100644
---
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkStorage.java
+++
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/ChunkStorage.java
@@ -35,6 +35,13 @@ public interface ChunkStorage {
*/
long size();
+ /**
+ * Gets the number of chunks accumulated so far.
+ *
+ * @return Number of appended chunks.
+ */
+ long count();
+
/**
* Retrieves final result from segmented payload.
*
diff --git
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/MemoryChunkStorage.java
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/MemoryChunkStorage.java
index d5b36feb82..a95d268342 100644
---
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/MemoryChunkStorage.java
+++
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/chunk/MemoryChunkStorage.java
@@ -38,6 +38,11 @@ public class MemoryChunkStorage implements ChunkStorage {
return size;
}
+ @Override
+ public long count() {
+ return chunks.size();
+ }
+
@Override
public byte[] get() {
Optional<byte[]> collect = chunks.stream().reduce((b1, b2) -> {
diff --git
a/plc4j/spi/fields/src/main/java/org/apache/plc4x/java/spi/fields/fields/reader/FieldReaderArray.java
b/plc4j/spi/fields/src/main/java/org/apache/plc4x/java/spi/fields/fields/reader/FieldReaderArray.java
index 0a2ba56f1e..9f454bb67f 100644
---
a/plc4j/spi/fields/src/main/java/org/apache/plc4x/java/spi/fields/fields/reader/FieldReaderArray.java
+++
b/plc4j/spi/fields/src/main/java/org/apache/plc4x/java/spi/fields/fields/reader/FieldReaderArray.java
@@ -35,6 +35,14 @@ public class FieldReaderArray<T> implements FieldCommons {
private static final Logger LOGGER =
LoggerFactory.getLogger(FieldReaderArray.class);
+ /**
+ * Upper bound for the initial capacity of an array whose element count
comes from the wire.
+ * Prevents a bogus, attacker-controlled count from triggering an eager
multi-GB allocation
+ * before any element is actually read. The list still grows past this if
that many elements
+ * really are present on the wire.
+ */
+ private static final int MAX_INITIAL_CAPACITY = 1024;
+
public List<T> readArrayFieldCount(DataReader<T> dataReader, long count,
WithOption... options) throws BufferException {
LOGGER.debug("reading field {}. Count: {}", getName(options), count);
if (count > Integer.MAX_VALUE) {
@@ -47,7 +55,11 @@ public class FieldReaderArray<T> implements FieldCommons {
//readerArgs = ArrayUtils.add(readerArgs,
WithReaderWriterArgs.WithRenderAsList(true));
dataReader.pushContext(options);
int itemCount = Math.max(0, (int) count);
- List<T> result = new ArrayList<>(itemCount);
+ // Don't eagerly pre-size the backing array to an untrusted,
wire-supplied count: a bogus
+ // count (e.g. 0x7fffffff) would trigger a multi-GB allocation before
a single element is
+ // read (OOM / DoS). Cap the initial capacity to a modest bound and
let the list grow as
+ // elements are actually decoded - once the buffer is exhausted,
dataReader.read() throws.
+ List<T> result = new ArrayList<>(Math.min(itemCount,
MAX_INITIAL_CAPACITY));
for (int curItem = 0; curItem < itemCount; curItem++) {
// Make some variables available that would be otherwise
challenging to forward.
ThreadLocalHelper.curItemThreadLocal.set(curItem);