CVE-2022-25897: Allocation of Resources Without Limits or Throttling (CWE-770)
value: The package org.eclipse.milo:sdk-server before 0.6.8 are vulnerable to Denial of Service (DoS) when bypassing the limitations for excessive memory consumption by sending multiple CloseSession requests with the deleteSubscription parameter equal to False.
CWE description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
CVSS v3 base score: 7.5
Published: 2022-09-08T05:15Z
Repository: https://github.com/eclipse/milo
Language: Java
Files changed: opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/api/config/OpcUaServerConfigLimits.java, opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/subscriptions/SubscriptionManager.java
Commit date: 2022-08-24 06:14:36 -0700
Commit message: Allow max MonitoredItems per session to be configured via OpcUaServerConfigLimits

Diff: diff --git a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/api/config/OpcUaServerConfigLimits.java b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/api/config/OpcUaServerConfigLimits.java
index 6525fa11..b57c5681 100644
--- a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/api/config/OpcUaServerConfigLimits.java
+++ b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/api/config/OpcUaServerConfigLimits.java
@@ -110,10 +110,19 @@ public interface OpcUaServerConfigLimits {
      */
     default UInteger getMaxMonitoredItems() {
         return uint(Integer.MAX_VALUE);
     }
 
+    /**
+     * Get the maximum number of MonitoredItems, per Session, that can be created.
+     *
+     * @return the maximum number of MonitoredItems, per Session, that can be created.
+     */
+    default UInteger getMaxMonitoredItemsPerSession() {
+        return uint(Integer.MAX_VALUE);
+    }
+
     //region ServerCapabilities
 
     default Double getMinSupportedSampleRate() {
         return 0.0;
     }
diff --git a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/subscriptions/SubscriptionManager.java b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/subscriptions/SubscriptionManager.java
index 1ab72420..71794f8f 100644
--- a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/subscriptions/SubscriptionManager.java
+++ b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/subscriptions/SubscriptionManager.java
@@ -120,10 +120,12 @@ public class SubscriptionManager {
     private final PublishQueue publishQueue = new PublishQueue();
 
     private final Map<UInteger, Subscription> subscriptions = Maps.newConcurrentMap();
     private final List<Subscription> transferred = Lists.newCopyOnWriteArrayList();
 
+    private final AtomicLong monitoredItemCount = new AtomicLong(0L);
+
     private final Session session;
     private final OpcUaServer server;
 
     public SubscriptionManager(Session session, OpcUaServer server) {
         this.session = session;
@@ -188,10 +190,11 @@ public class SubscriptionManager {
                     monitoredItems.values(),
                     dataItems -> server.getAddressSpaceManager().onDataItemsDeleted(dataItems),
                     eventItems -> server.getAddressSpaceManager().onEventItemsDeleted(eventItems)
                 );
 
+                monitoredItemCount.getAndUpdate(count -> count - monitoredItems.size());
                 server.getMonitoredItemCount().getAndUpdate(count -> count - monitoredItems.size());
 
                 monitoredItems.clear();
             }
         });
@@ -265,10 +268,11 @@ public class SubscriptionManager {
                     eventItems -> server.getAddressSpaceManager().onEventItemsDeleted(eventItems)
                 );
 
                 results[i] = StatusCode.GOOD;
 
+                monitoredItemCount.getAndUpdate(count -> count - deletedItems.size());
                 server.getMonitoredItemCount().getAndUpdate(count -> count - deletedItems.size());
             } else {
                 results[i] = new StatusCode(StatusCodes.Bad_SubscriptionIdInvalid);
             }
         }
@@ -346,17 +350,24 @@ public class SubscriptionManager {
         attributesFuture.thenAccept(attributeGroups -> {
             MonitoredItemCreateResult[] createResults = new MonitoredItemCreateResult[itemsToCreate.size()];
 
             List<BaseMonitoredItem<?>> monitoredItems = new ArrayList<>();
 
+            long globalMax = server.getConfig()
+                .getLimits().getMaxMonitoredItems().longValue();
+
+            long sessionMax = server.getConfig()
+                .getLimits().getMaxMonitoredItemsPerSession().longValue();
+
             for (int i = 0; i < itemsToCreate.size(); i++) {
                 MonitoredItemCreateRequest createRequest = itemsToCreate.get(i);
 
                 try {
-                    long maxMonitoredItems = server.getConfig().getLimits().getMaxMonitoredItems().longValue();
+                    long globalCount = server.getMonitoredItemCount().incrementAndGet();
+                    long sessionCount = monitoredItemCount.incrementAndGet();
 
-                    if (server.getMonitoredItemCount().incrementAndGet() <= maxMonitoredItems) {
+                    if (globalCount <= globalMax && sessionCount <= sessionMax) {
                         BaseMonitoredItem<?> monitoredItem = createMonitoredItem(
                             createRequest,
                             subscription,
                             timestamps,
                             attributeGroups
@@ -373,10 +384,11 @@ public class SubscriptionManager {
                         );
                     } else {
                         throw new UaException(StatusCodes.Bad_TooManyMonitoredItems);
                     }
                 } catch (UaException e) {
+                    monitoredItemCount.decrementAndGet();
                     server.getMonitoredItemCount().decrementAndGet();
 
                     createResults[i] = new MonitoredItemCreateResult(
                         e.getStatusCode(),
                         UInteger.MIN,
@@ -977,10 +989,11 @@ public class SubscriptionManager {
                 } else {
                     deletedItems.add(item);
 
                     deleteResults[i] = StatusCode.GOOD;
 
+                    monitoredItemCount.decrementAndGet();
                     server.getMonitoredItemCount().decrementAndGet();
                 }
             }
 
             subscription.removeMonitoredItems(deletedItems);
@@ -1248,10 +1261,11 @@ public class SubscriptionManager {
                     deletedItems,
                     dataItems -> server.getAddressSpaceManager().onDataItemsDeleted(dataItems),
                     eventItems -> server.getAddressSpaceManager().onEventItemsDeleted(eventItems)
                 );
 
+                monitoredItemCount.getAndUpdate(count -> count - deletedItems.size());
                 server.getMonitoredItemCount().getAndUpdate(count -> count - deletedItems.size());
             }
 
             iterator.remove();
         }
@@ -1291,10 +1305,11 @@ public class SubscriptionManager {
                     monitoredItems.values(),
                     dataItems -> server.getAddressSpaceManager().onDataItemsDeleted(dataItems),
                     eventItems -> server.getAddressSpaceManager().onEventItemsDeleted(eventItems)
                 );
 
+                monitoredItemCount.getAndUpdate(count -> count - monitoredItems.size());
                 server.getMonitoredItemCount().getAndUpdate(count -> count - monitoredItems.size());
 
                 monitoredItems.clear();
             }
         });
@@ -1310,10 +1325,12 @@ public class SubscriptionManager {
         Subscription subscription = subscriptions.remove(subscriptionId);
         server.getEventBus().post(new SubscriptionDeletedEvent(subscription));
 
         if (subscription != null) {
             subscription.setStateListener(null);
+
+            monitoredItemCount.getAndUpdate(count -> count - subscription.getMonitoredItems().size());
         }
 
         return subscription;
     }
 

Vulnerable code:                     long maxMonitoredItems = server.getConfig().getLimits().getMaxMonitoredItems().longValue();
                    if (server.getMonitoredItemCount().incrementAndGet() <= maxMonitoredItems) {
Fixed code:     /**
     * Get the maximum number of MonitoredItems, per Session, that can be created.
     *
     * @return the maximum number of MonitoredItems, per Session, that can be created.
     */
    default UInteger getMaxMonitoredItemsPerSession() {
        return uint(Integer.MAX_VALUE);
    }

    private final AtomicLong monitoredItemCount = new AtomicLong(0L);

                monitoredItemCount.getAndUpdate(count -> count - monitoredItems.size());
                monitoredItemCount.getAndUpdate(count -> count - deletedItems.size());
            long globalMax = server.getConfig()
                .getLimits().getMaxMonitoredItems().longValue();

            long sessionMax = server.getConfig()
                .getLimits().getMaxMonitoredItemsPerSession().longValue();

                    long globalCount = server.getMonitoredItemCount().incrementAndGet();
                    long sessionCount = monitoredItemCount.incrementAndGet();
                    if (globalCount <= globalMax && sessionCount <= sessionMax) {
                    monitoredItemCount.decrementAndGet();
                    monitoredItemCount.decrementAndGet();
                monitoredItemCount.getAndUpdate(count -> count - deletedItems.size());
                monitoredItemCount.getAndUpdate(count -> count - monitoredItems.size());

            monitoredItemCount.getAndUpdate(count -> count - subscription.getMonitoredItems().size());