From 019b2c69bd5b8cf8fa9a21bd5c4479204b27cb36 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 08 Jul 2026 16:05:46 +0000
Subject: [PATCH] [#665] Fix StackOverflowError while parsing long ACI with repetitive targets (#666)
---
opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/TargetAttr.java | 12 +-
opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/Aci.java | 63 +++++++++---
opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/AciTargets.java | 16 ++-
opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/Permission.java | 15 ++-
opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/AciDecodeStackOverflowTestCase.java | 162 ++++++++++++++++++++++++++++++++
5 files changed, 234 insertions(+), 34 deletions(-)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/Aci.java b/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/Aci.java
index 8e69671..d938a09 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/Aci.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/Aci.java
@@ -13,6 +13,7 @@
*
* Copyright 2008 Sun Microsystems, Inc.
* Portions Copyright 2010-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC
*/
package org.opends.server.authorization.dseecompat;
@@ -45,12 +46,26 @@
/** Regular expression matching a word group. */
public static final String WORD_GROUP="(\\w+)";
+ /**
+ * Possessive variant of {@link #WORD_GROUP}. Use inside unbounded
+ * repetition so the regex engine matches iteratively instead of
+ * recursing once per repetition (issue #665).
+ */
+ public static final String WORD_GROUP_POSSESSIVE="(\\w++)";
+
/** Regular expression matching a word group at the start of a pattern. */
static final String WORD_GROUP_START_PATTERN = "^" + WORD_GROUP;
/** Regular expression matching a white space. */
public static final String ZERO_OR_MORE_WHITESPACE="\\s*";
+ /**
+ * Possessive variant of {@link #ZERO_OR_MORE_WHITESPACE}. Use inside
+ * unbounded repetition so the regex engine matches iteratively instead of
+ * recursing once per repetition (issue #665).
+ */
+ public static final String ZERO_OR_MORE_WHITESPACE_POSSESSIVE="\\s*+";
+
/** Regular expression matching a white space at the start of a pattern. */
public static final String ZERO_OR_MORE_WHITESPACE_START_PATTERN =
"^" + ZERO_OR_MORE_WHITESPACE ;
@@ -106,13 +121,17 @@
"\\+" + ZERO_OR_MORE_WHITESPACE;
/** Regular expression used to do quick check of OID string. */
- private static final String OID_NAME = "[\\d.\\*]*";
+ private static final String OID_NAME = "[\\d.\\*]*+";
- /** Regular expression that matches one or more OID_NAME's separated by the "||" token. */
- private static final String oidListRegex = ZERO_OR_MORE_WHITESPACE +
- OID_NAME + ZERO_OR_MORE_WHITESPACE + "(" +
- LOGICAL_OR + ZERO_OR_MORE_WHITESPACE + OID_NAME +
- ZERO_OR_MORE_WHITESPACE + ")*";
+ /**
+ * Regular expression that matches one or more OID_NAME's separated by the
+ * "||" token. Possessive quantifiers keep the unbounded repetition from
+ * recursing in the regex engine (issue #665).
+ */
+ private static final String oidListRegex = ZERO_OR_MORE_WHITESPACE_POSSESSIVE +
+ OID_NAME + ZERO_OR_MORE_WHITESPACE_POSSESSIVE + "(" +
+ LOGICAL_OR + ZERO_OR_MORE_WHITESPACE_POSSESSIVE + OID_NAME +
+ ZERO_OR_MORE_WHITESPACE_POSSESSIVE + ")*+";
/** ACI_ADD is used to set the container rights for a LDAP add operation. */
public static final int ACI_ADD = 0x0020;
@@ -247,19 +266,27 @@
public static Aci decode (ByteSequence byteString, DN dn)
throws AciException {
String input=byteString.toString();
- //Perform a quick pattern check against the string to catch any
- //obvious syntax errors.
- if (!Pattern.matches(aciRegex, input)) {
- throw new AciException(WARN_ACI_SYNTAX_GENERAL_PARSE_FAILED.get(input));
+ try {
+ //Perform a quick pattern check against the string to catch any
+ //obvious syntax errors.
+ if (!Pattern.matches(aciRegex, input)) {
+ throw new AciException(WARN_ACI_SYNTAX_GENERAL_PARSE_FAILED.get(input));
+ }
+ //Decode the body first.
+ AciBody body=AciBody.decode(input);
+ //Create a substring from the start of the string to start of
+ //the body. That should be the target.
+ String targetStr = input.substring(0, body.getMatcherStartPos());
+ //Decode that target string using the substring.
+ AciTargets targets=AciTargets.decode(targetStr, dn);
+ return new Aci(input, dn, body, targets);
+ } catch (StackOverflowError e) {
+ //Defense in depth: a pathological ACI can overflow the regex
+ //engine's recursion. StackOverflowError is an Error and would
+ //escape catch(Exception) handlers, killing the worker thread, so
+ //treat the value as an invalid ACI instead (issue #665).
+ throw new AciException(WARN_ACI_SYNTAX_GENERAL_PARSE_FAILED.get(input), e);
}
- //Decode the body first.
- AciBody body=AciBody.decode(input);
- //Create a substring from the start of the string to start of
- //the body. That should be the target.
- String targetStr = input.substring(0, body.getMatcherStartPos());
- //Decode that target string using the substring.
- AciTargets targets=AciTargets.decode(targetStr, dn);
- return new Aci(input, dn, body, targets);
}
/**
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/AciTargets.java b/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/AciTargets.java
index 79c6f29..f7e34d2 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/AciTargets.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/AciTargets.java
@@ -13,6 +13,7 @@
*
* Copyright 2008-2010 Sun Microsystems, Inc.
* Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC
*/
package org.opends.server.authorization.dseecompat;
@@ -63,16 +64,19 @@
/** Regular expression used to match a single target rule. */
private static final String targetRegex =
- OPEN_PAREN + ZERO_OR_MORE_WHITESPACE + WORD_GROUP +
- ZERO_OR_MORE_WHITESPACE + "(!?=)" + ZERO_OR_MORE_WHITESPACE +
- "\"([^\"]+)\"" + ZERO_OR_MORE_WHITESPACE + CLOSED_PAREN +
- ZERO_OR_MORE_WHITESPACE;
+ OPEN_PAREN + ZERO_OR_MORE_WHITESPACE_POSSESSIVE + WORD_GROUP_POSSESSIVE +
+ ZERO_OR_MORE_WHITESPACE_POSSESSIVE + "(!?=)" + ZERO_OR_MORE_WHITESPACE_POSSESSIVE +
+ "\"([^\"]+)\"" + ZERO_OR_MORE_WHITESPACE_POSSESSIVE + CLOSED_PAREN +
+ ZERO_OR_MORE_WHITESPACE_POSSESSIVE;
/**
* Regular expression used to match one or more target rules. The pattern is
- * part of a general ACI verification.
+ * part of a general ACI verification. The possessive quantifiers make the
+ * regex engine match the repetition iteratively, so an ACI with thousands
+ * of repeated target rules is rejected instead of overflowing the stack
+ * (issue #665).
*/
- static final String targetsRegex = "(" + targetRegex + ")*";
+ static final String targetsRegex = "(" + targetRegex + ")*+";
/**
* Rights that are skipped for certain target evaluations.
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/Permission.java b/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/Permission.java
index 91b09c6..3e49a35 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/Permission.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/Permission.java
@@ -13,6 +13,7 @@
*
* Copyright 2008 Sun Microsystems, Inc.
* Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC
*/
package org.opends.server.authorization.dseecompat;
@@ -34,11 +35,15 @@
/** Regular expression token representing the separator. */
private static final String separatorToken = ",";
- /** Regular expression used to match the ACI rights string. */
- private static final String rightsRegex = ZERO_OR_MORE_WHITESPACE +
- WORD_GROUP + ZERO_OR_MORE_WHITESPACE +
- "(," + ZERO_OR_MORE_WHITESPACE + WORD_GROUP +
- ZERO_OR_MORE_WHITESPACE + ")*";
+ /**
+ * Regular expression used to match the ACI rights string. Possessive
+ * quantifiers keep the unbounded repetition from recursing in the regex
+ * engine (issue #665).
+ */
+ private static final String rightsRegex = ZERO_OR_MORE_WHITESPACE_POSSESSIVE +
+ WORD_GROUP_POSSESSIVE + ZERO_OR_MORE_WHITESPACE_POSSESSIVE +
+ "(," + ZERO_OR_MORE_WHITESPACE_POSSESSIVE + WORD_GROUP_POSSESSIVE +
+ ZERO_OR_MORE_WHITESPACE_POSSESSIVE + ")*+";
/** The access type (allow,deny) corresponding to the ACI permission value. */
private final EnumAccessType accessType;
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/TargetAttr.java b/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/TargetAttr.java
index 3c581ce..c07a6c4 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/TargetAttr.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/TargetAttr.java
@@ -13,6 +13,7 @@
*
* Copyright 2008 Sun Microsystems, Inc.
* Portions Copyright 2012-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC
*/
package org.opends.server.authorization.dseecompat;
@@ -45,12 +46,13 @@
/**
* Regular expression that matches one or more ATTR_NAME's separated by
- * the "||" token.
+ * the "||" token. Possessive quantifiers keep the unbounded repetition
+ * from recursing in the regex engine (issue #665).
*/
- private static final String attrListRegex = ZERO_OR_MORE_WHITESPACE +
- ATTR_NAME + ZERO_OR_MORE_WHITESPACE + "(" +
- LOGICAL_OR + ZERO_OR_MORE_WHITESPACE + ATTR_NAME +
- ZERO_OR_MORE_WHITESPACE + ")*";
+ private static final String attrListRegex = ZERO_OR_MORE_WHITESPACE_POSSESSIVE +
+ ATTR_NAME + ZERO_OR_MORE_WHITESPACE_POSSESSIVE + "(" +
+ LOGICAL_OR + ZERO_OR_MORE_WHITESPACE_POSSESSIVE + ATTR_NAME +
+ ZERO_OR_MORE_WHITESPACE_POSSESSIVE + ")*+";
/**
* Constructor creating a class representing a targetattr keyword of an ACI.
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/AciDecodeStackOverflowTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/AciDecodeStackOverflowTestCase.java
new file mode 100644
index 0000000..535f584
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/AciDecodeStackOverflowTestCase.java
@@ -0,0 +1,162 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.server.authorization.dseecompat;
+
+import java.util.regex.Pattern;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.types.DirectoryException;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import static org.assertj.core.api.Assertions.*;
+
+/**
+ * Regression tests for issue #665: an ACI with thousands of repeated target
+ * rules used to overflow the regex engine's recursion in
+ * {@code Pattern.matches(aciRegex, ...)}, throwing StackOverflowError instead
+ * of the expected AciException.
+ */
+@SuppressWarnings("javadoc")
+public class AciDecodeStackOverflowTestCase extends DirectoryServerTestCase
+{
+ /** Enough repeated target rules to overflow an unbounded regex recursion. */
+ private static final int REPEATED_TARGETS = 10000;
+ /**
+ * A small bounded stack makes any per-repetition regex recursion overflow
+ * deterministically, while the iterative (possessive) match needs far less.
+ */
+ private static final long DECODE_STACK_SIZE_BYTES = 256 * 1024;
+
+ @BeforeClass
+ public void setUp() throws Exception
+ {
+ TestCaseUtils.startFakeServer();
+ }
+
+ @AfterClass
+ public void tearDown() throws DirectoryException
+ {
+ TestCaseUtils.shutdownFakeServer();
+ }
+
+ @Test
+ public void repeatedTargetsAciIsRejectedWithoutStackOverflow() throws Exception
+ {
+ StringBuilder aci = new StringBuilder();
+ for (int i = 0; i < REPEATED_TARGETS; i++)
+ {
+ aci.append("(targetscope=\"&O\")");
+ }
+ aci.append("(version 3.0; acl \"\"; allow (read) userdn=\"ldap:///anyone\";)");
+ final String input = aci.toString();
+
+ Throwable thrown = runOnBoundedStack(new Task()
+ {
+ @Override
+ public void run() throws Exception
+ {
+ Aci.decode(ByteString.valueOfUtf8(input), DN.rootDN());
+ }
+ });
+ assertThat(thrown)
+ .as("pathological ACI must be rejected with AciException")
+ .isInstanceOf(AciException.class);
+ }
+
+ /**
+ * Checks the regex itself matches iteratively, independently of the
+ * defense-in-depth catch in Aci.decode.
+ */
+ @Test
+ public void targetsRegexMatchesIterativelyOnBoundedStack() throws Exception
+ {
+ StringBuilder targets = new StringBuilder();
+ for (int i = 0; i < REPEATED_TARGETS; i++)
+ {
+ targets.append("(targetscope=\"&O\")");
+ }
+ final String input = targets.toString();
+
+ final boolean[] matches = new boolean[1];
+ Throwable thrown = runOnBoundedStack(new Task()
+ {
+ @Override
+ public void run()
+ {
+ matches[0] = Pattern.matches(AciTargets.targetsRegex, input);
+ }
+ });
+ assertThat(thrown).as("targets regex must not recurse per repetition").isNull();
+ assertThat(matches[0]).isTrue();
+ }
+
+ @Test
+ public void validAciWithSeveralTargetsStillDecodes() throws Exception
+ {
+ String aci = "(targetattr=\"cn\")(targetscope=\"onelevel\")"
+ + "(version 3.0; acl \"test\"; allow (read,search) userdn=\"ldap:///anyone\";)";
+ assertThat(Aci.decode(ByteString.valueOfUtf8(aci), DN.rootDN()).toString()).isEqualTo(aci);
+ }
+
+ /** The possessive target regex must still reject malformed target syntax. */
+ @Test
+ public void malformedTargetStillRejected() throws Exception
+ {
+ final String aci = "(targetscope \"onelevel\")"
+ + "(version 3.0; acl \"test\"; allow (read) userdn=\"ldap:///anyone\";)";
+ assertThatThrownBy(() -> Aci.decode(ByteString.valueOfUtf8(aci), DN.rootDN()))
+ .isInstanceOf(AciException.class);
+ }
+
+ /** A runnable that may throw a checked exception (e.g. AciException). */
+ private interface Task
+ {
+ void run() throws Exception;
+ }
+
+ /**
+ * Runs the task on a thread with a small fixed stack and returns whatever
+ * it threw, including Errors, or null if it completed.
+ */
+ private Throwable runOnBoundedStack(final Task task) throws InterruptedException
+ {
+ final Throwable[] thrown = new Throwable[1];
+ Runnable guarded = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ task.run();
+ }
+ catch (Throwable t)
+ {
+ thrown[0] = t;
+ }
+ }
+ };
+ Thread worker = new Thread(null, guarded, "issue-665-bounded-stack", DECODE_STACK_SIZE_BYTES);
+ worker.start();
+ worker.join();
+ return thrown[0];
+ }
+}
--
Gitblit v1.10.0