mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
16 hours ago 019b2c69bd5b8cf8fa9a21bd5c4479204b27cb36
[#665] Fix StackOverflowError while parsing long ACI with repetitive targets (#666)
4 files modified
1 files added
244 ■■■■■ changed files
opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/Aci.java 39 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/AciTargets.java 16 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/Permission.java 15 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/TargetAttr.java 12 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/AciDecodeStackOverflowTestCase.java 162 ●●●●● patch | view | raw | blame | history
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];
  }
}