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,6 +266,7 @@ public static Aci decode (ByteSequence byteString, DN dn) throws AciException { String input=byteString.toString(); try { //Perform a quick pattern check against the string to catch any //obvious syntax errors. if (!Pattern.matches(aciRegex, input)) { @@ -260,6 +280,13 @@ //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); } } /** 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. 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; 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. opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/AciDecodeStackOverflowTestCase.java
New file @@ -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]; } }