Skip to content

[#843] Cap the number of operations accepted per batchRequest in the DSML gateway - #844

Merged
vharseko merged 5 commits into
OpenIdentityPlatform:masterfrom
vharseko:issues/843-dsml-batchrequest-operations-cap
Aug 5, 2026
Merged

[#843] Cap the number of operations accepted per batchRequest in the DSML gateway#844
vharseko merged 5 commits into
OpenIdentityPlatform:masterfrom
vharseko:issues/843-dsml-batchrequest-operations-cap

Conversation

@vharseko

@vharseko vharseko commented Aug 4, 2026

Copy link
Copy Markdown
Member

Split out of the review of #835.

#835 capped the number of batchRequest elements per SOAP body (each element costs its own connection and bind), but the number of operations inside one batchRequest stayed unbounded: within the 10 MiB request-size cap a single body holds on the order of 90k <compareRequest> elements asserting userPassword, and a compare against an attribute stored under a salted password scheme (PBKDF2, bcrypt, …) costs the same deliberately slow password verification as a bind — one authenticated POST bought an unbounded amount of CPU.

A new web.xml context-param ldap.dsml.batchrequest.operations.max now caps the operation count per batchRequest. The default is generous (10000) because large batches are a designed use of DSMLv2 (bulk provisioning); raise it explicitly for bigger bulk loads. A batch over the cap is rejected as a whole with a single notAttempted errorResponse, before the gateway even opens the LDAP connection: the count is known up front once the batch is unmarshalled, notAttempted is then literally true, and a provisioning batch applied halfway is worse than one not attempted. The configured value is not echoed to the client, and a non-positive value is rejected at servlet init, matching the #835 caps.

Tests: a batch over the cap is refused without a single connection to the directory server and keeps the requestID for correlation; a batch of exactly the cap is fully processed; the init-time validation covers the new parameter.

Release note

The DSML gateway now accepts at most 10000 operations per batchRequest. Bulk-provisioning clients that send larger batches must raise ldap.dsml.batchrequest.operations.max in web.xml, or split their batches. Batches over the cap are rejected as a whole with a notAttempted errorResponse. The default lives in the servlet, so an existing deployment inherits it on upgrade without touching its web.xml.

Stacked on #835: the first two commits are that PR, only aa471dba74 is new here. Merge #835 first.

Fixes #843

…nd the request body size in the DSML gateway

Every batchRequest element of a SOAP body is executed over its own
connection and bind, and password verification is deliberately expensive,
so a small POST holding many batchRequest elements amplified into many
binds; the SOAP message is also parsed into memory, so an unbounded body
was an unbounded allocation. Cap both: ldap.dsml.batchrequests.max
(default 1, as DSMLv2 describes a single batchRequest per body) and
ldap.dsml.request.maxsize (default 10485760 bytes). Excess elements and
oversized bodies are rejected with a notAttempted errorResponse; the
declared Content-Length is refused without reading the body, and chunked
bodies are capped while streamed.
Hoist the declared Content-Length check above the malformed-Content-Type
fallback and guard it on an empty batchResponses: it used to add a second
errorResponse next to a credentials error, and let the fallback SAX-parse
an oversized body to recover the requestID. Stop echoing the configured
caps to the unauthenticated client and drop the word batchRequest from
the excess message, which counts every element of the SOAP body. Parse
the context-params without NumberFormatException as control flow, reuse
positiveValue() for ldap.dsml.dereference.anyuri.maxsize, rethrow
ServletException out of init() instead of printStackTrace(), and close
the request input stream with try-with-resources (CodeQL
java/input-resource-leak).

New tests: a credentials error is not doubled by the size check, an
oversized body without a Content-Type is rejected unread with a single
error and no requestID, a body of exactly the cap is accepted, and the
excess-batch test now proves partial search results arrive next to the
notAttempted error.
…batchRequest in the DSML gateway

A compare on an attribute stored under a salted password scheme costs a
full password verification, so an unbounded batch let a single
authenticated POST buy on the order of 90k expensive verifications
within the request-size cap. A batchRequest holding more operations than
ldap.dsml.batchrequest.operations.max (default 10000: large batches are
a designed use of DSMLv2) is now rejected as a whole with a notAttempted
errorResponse before the gateway even connects.
@vharseko
vharseko requested a review from maximthomas August 4, 2026 07:07
@vharseko vharseko added security Security fixes / CodeQL code-scanning alerts enhancement java Pull requests that update java code labels Aug 4, 2026

@maximthomas maximthomas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Merge preconditions

  1. Merge #835 first. It is still CHANGES_REQUESTED; the requested fixes are present in
    102f1ecee9 on this branch, so it needs a re-review, not new work.

  2. Release-note the new default. Unlike #835's ldap.dsml.batchrequests.max=1 — which broke
    nothing, because a second batchRequest element never worked before #811 — a cap of 10000
    operations can reject batches that work today. DEFAULT_MAX_OPERATIONS lives in code
    (DSMLServlet.java:149), so an existing deployment inherits it on upgrade without touching
    its web.xml. Suggested note:

    The DSML gateway now accepts at most 10000 operations per batchRequest. Bulk-provisioning
    clients that send larger batches must raise ldap.dsml.batchrequest.operations.max in
    web.xml, or split their batches. Batches over the cap are rejected as a whole with a
    notAttempted errorResponse.


Severity note for the issue/PR text

The PR body says "one authenticated POST bought an unbounded amount of CPU". Verified against
this repo, that is right, and slightly conservative:

  • opendj-server-legacy/resource/config/config.ldif:92 — the stock global ACI "Self entry read"
    grants (read,search,compare) on userPassword||authPassword to ldap:///self. Any
    authenticated user can self-compare their own password, so the amplifier is reachable under
    default ACIs. (config.ldif:88 only withholds it for other entries.)
  • config.ldif:998 — the default storage scheme is Salted SHA-1, which is cheap. The expensive
    case needs PBKDF2/bcrypt configured, which the PR body already states correctly.
  • The shipped web.xml example for ldap.userdn is cn=Directory Manager (commented out). A
    gateway deployed that way binds as a root DN, bypasses ACIs entirely, and the amplifier becomes
    pre-authentication — worth one sentence in the issue

@vharseko
vharseko merged commit 924eb4a into OpenIdentityPlatform:master Aug 5, 2026
17 checks passed
@vharseko
vharseko deleted the issues/843-dsml-batchrequest-operations-cap branch August 5, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement java Pull requests that update java code security Security fixes / CodeQL code-scanning alerts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DSML gateway: cap the number of operations per batchRequest, a compare on userPassword costs a password verification

2 participants