opendj-dsml-servlet/resources/webapp/web.xml
@@ -126,6 +126,21 @@ <param-value>10485760</param-value> </context-param> <context-param> <description>Maximum number of operations accepted in one batchRequest. A compare on an attribute stored under a salted password scheme (PBKDF2, bcrypt, ...) costs a full password verification, so an unbounded batch lets a single POST buy an unbounded amount of CPU. Large batches are a designed use of DSMLv2 (bulk provisioning), so the default is generous; raise it if your clients really send more. A batchRequest holding more operations is rejected as a whole with a notAttempted errorResponse, before anything is executed. Note that a single POST may hold up to ldap.dsml.batchrequests.max batchRequest elements, each allowed this many operations.</description> <param-name>ldap.dsml.batchrequest.operations.max</param-name> <param-value>10000</param-value> </context-param> <!-- Add an extra <context-param> like the one below for each extended operation that is known to return a string in the LDAP response. --> <context-param> 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 opendj-dsml-servlet/src/test/java/org/opends/dsml/protocol/DSMLServletTestCase.java
@@ -79,7 +79,9 @@ * body used to be silently skipped, and every batch request of a SOAP body * used to be answered inside a single shared batchResponse. Also covers the * caps on the number of batchRequest elements per SOAP body (each element * costs a bind) and on the size of the request body. * costs a bind), on the size of the request body, and on the number of * operations per batchRequest (a compare on a password attribute costs a * password verification). */ @SuppressWarnings("javadoc") @Test(groups = { "precommit", "dsml" }) @@ -125,6 +127,18 @@ + "</batchRequest>"; } /** A batch request holding the given number of abandon operations. */ private static String multiOperationBatch(String requestID, int operationCount) { StringBuilder batch = new StringBuilder( "<batchRequest xmlns=\"urn:oasis:names:tc:DSML:2:0:core\" requestID=\"" + requestID + "\">"); for (int i = 1; i <= operationCount; i++) { batch.append("<abandonRequest abandonID=\"").append(i).append("\"/>"); } return batch.append("</batchRequest>").toString(); } private static String searchBatch(String requestID) { return "<batchRequest xmlns=\"urn:oasis:names:tc:DSML:2:0:core\" requestID=\"" + requestID + "\">" @@ -458,6 +472,59 @@ } } /** * A batchRequest holding more operations than the configured cap is rejected * as a whole before the gateway even connects: a compare on a password * attribute costs a password verification, and a provisioning batch applied * halfway is worse than one not attempted. The requestID is kept so that the * client can correlate the reply. */ @Test public void testBatchHoldingMoreOperationsThanTheCapIsRejectedWhole() throws Exception { try (FakeLdapServer server = new FakeLdapServer()) { Map<String, String> params = new LinkedHashMap<>(); params.put("ldap.dsml.batchrequest.operations.max", "2"); Map<String, String> headers = new LinkedHashMap<>(); headers.put("Content-Type", SOAP_1_1_CONTENT_TYPE); String response = doPost(server.getPort(), params, headers, soap11(multiOperationBatch("1", 3))); assertTrue(response.contains("notAttempted"), response); assertTrue(response.contains("requestID=\"1\""), response); assertTrue(server.getReceivedOpTypes().isEmpty(), "no connection to the directory server should have been opened"); } } /** A batch of exactly the configured maximum is accepted: the cap fails only past the limit. */ @Test public void testBatchOfExactlyTheMaximumOperationsIsAccepted() throws Exception { try (FakeLdapServer server = new FakeLdapServer()) { Map<String, String> params = new LinkedHashMap<>(); params.put("ldap.dsml.batchrequest.operations.max", "2"); Map<String, String> headers = new LinkedHashMap<>(); headers.put("Content-Type", SOAP_1_1_CONTENT_TYPE); String response = doPost(server.getPort(), params, headers, soap11(multiOperationBatch("1", 2))); assertFalse(response.contains("errorResponse"), response); server.awaitDisconnect(); assertEquals(server.getReceivedOpTypes(), list(OP_TYPE_BIND_REQUEST, OP_TYPE_ABANDON_REQUEST, OP_TYPE_ABANDON_REQUEST, OP_TYPE_UNBIND_REQUEST), "a batch of exactly the configured maximum must be processed"); } } /** A cap which is not a positive number must be rejected when the servlet initialises. */ @Test public void testNonPositiveCapsAreRejectedAtInit() throws Exception @@ -465,7 +532,8 @@ for (String[] param : new String[][] { { "ldap.dsml.batchrequests.max", "0" }, { "ldap.dsml.batchrequests.max", "banana" }, { "ldap.dsml.request.maxsize", "-1" } }) { "ldap.dsml.request.maxsize", "-1" }, { "ldap.dsml.batchrequest.operations.max", "0" } }) { Map<String, String> params = new LinkedHashMap<>(); params.put("ldap.host", InetAddress.getLoopbackAddress().getHostAddress());