From 924eb4a46d87170837d94291120a0fbcf0646f8c Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 05 Aug 2026 17:01:53 +0000
Subject: [PATCH] [#843] Cap the number of operations accepted per batchRequest in the DSML gateway (#844)

---
 opendj-dsml-servlet/src/main/java/org/opends/dsml/protocol/DSMLServlet.java |   36 ++++++++++++++++++++++++++++++++----
 1 files changed, 32 insertions(+), 4 deletions(-)

diff --git a/opendj-dsml-servlet/src/main/java/org/opends/dsml/protocol/DSMLServlet.java b/opendj-dsml-servlet/src/main/java/org/opends/dsml/protocol/DSMLServlet.java
index df2ba81..b1e787e 100644
--- a/opendj-dsml-servlet/src/main/java/org/opends/dsml/protocol/DSMLServlet.java
+++ b/opendj-dsml-servlet/src/main/java/org/opends/dsml/protocol/DSMLServlet.java
@@ -130,6 +130,7 @@
   private static final String DEREF_ANYURI_MAXSIZE = "ldap.dsml.dereference.anyuri.maxsize";
   private static final String MAX_BATCH_REQUESTS = "ldap.dsml.batchrequests.max";
   private static final String REQUEST_MAXSIZE = "ldap.dsml.request.maxsize";
+  private static final String MAX_OPERATIONS = "ldap.dsml.batchrequest.operations.max";
 
   /**
    * A SOAP body carries a single batchRequest element by default, as DSMLv2
@@ -138,6 +139,14 @@
   private static final long DEFAULT_MAX_BATCH_REQUESTS = 1;
   /** Default cap on the size of an accepted request body, in bytes. */
   private static final long DEFAULT_REQUEST_MAXSIZE = 10 * 1024 * 1024;
+  /**
+   * Default cap on the number of operations accepted in one batchRequest.
+   * Large batches are a designed use of DSMLv2 (bulk provisioning), so the
+   * default is generous; it exists because a compare on an attribute stored
+   * under a salted password scheme costs a full password verification, so an
+   * unbounded batch would let a single POST buy an unbounded amount of CPU.
+   */
+  private static final long DEFAULT_MAX_OPERATIONS = 10000;
   private static final long serialVersionUID = -3748022009593442973L;
   private static final AtomicInteger nextMessageID = new AtomicInteger(1);
 
@@ -170,6 +179,7 @@
   private Boolean useHTTPAuthzID;
   private long maxBatchRequests;
   private long requestMaxSize;
+  private long maxOperations;
   private final Set<String> exopStrings = new HashSet<>();
 
   /**
@@ -240,12 +250,14 @@
       }
 
       // Every batchRequest element of a SOAP body is executed over its own
-      // connection and bind, and password verification is deliberately
-      // expensive: cap how many binds a single POST may fan out into, and how
-      // much memory its body may claim, so that a small request cannot buy
-      // unbounded work.
+      // connection and bind, password verification is deliberately expensive,
+      // and a compare on a password attribute costs one too: cap how many
+      // binds a single POST may fan out into, how much memory its body may
+      // claim, and how many operations one batchRequest may hold, so that a
+      // small request cannot buy unbounded work.
       maxBatchRequests = positiveValue(config, MAX_BATCH_REQUESTS, DEFAULT_MAX_BATCH_REQUESTS);
       requestMaxSize = positiveValue(config, REQUEST_MAXSIZE, DEFAULT_REQUEST_MAXSIZE);
+      maxOperations = positiveValue(config, MAX_OPERATIONS, DEFAULT_MAX_OPERATIONS);
 
       if(jaxbContext==null)
       {
@@ -655,6 +667,22 @@
             boolean authzInControl = false;
             batchRequest = batchRequestElement.getValue();
 
+            if ( batchRequest.getBatchRequests().size() > maxOperations ) {
+              // A compare on an attribute stored under a salted password scheme
+              // costs a full password verification, so the number of operations
+              // one batchRequest may hold is capped (MAX_OPERATIONS). The batch
+              // is refused as a whole before the gateway even connects: a
+              // provisioning batch applied halfway is worse than one not
+              // attempted. The configured value is not echoed to the
+              // unauthenticated client.
+              elementResponse.setRequestID(batchRequest.getRequestID());
+              elementResponses.add(createErrorResponse(objFactory,
+                  new LDAPException(LDAPResultCode.UNWILLING_TO_PERFORM,
+                      LocalizableMessage.raw("The batchRequest holds more operations than the"
+                          + " configured maximum: none were attempted."))));
+              continue;
+            }
+
             // The connection options are shared by all the batch requests of this
             // SOAP body, so the authzid of the previous one must not survive into
             // the bind of this one: it would run under an authorization identity

--
Gitblit v1.10.0