From af673bb6dff871dcf5eeb1bbf2a324cf5acf8512 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Mon, 27 Jul 2026 17:22:14 +0000
Subject: [PATCH] Fix java/redos CodeQL alert in LDIFChangeRecordReader (#764)

---
 opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java |   95 +++++++++++++++++++++++++++++++++++++++++++++++
 opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java         |    3 +
 2 files changed, 97 insertions(+), 1 deletions(-)

diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java b/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java
index 58fe0ba..65d3b9f 100644
--- a/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java
+++ b/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java
@@ -14,6 +14,7 @@
  * Copyright 2009-2010 Sun Microsystems, Inc.
  * Portions copyright 2011-2016 ForgeRock AS.
  * Portions copyright 2016 Matthew Stevenson
+ * Portions Copyrighted 2026 3A Systems, LLC.
  */
 package org.forgerock.opendj.ldif;
 
@@ -82,7 +83,7 @@
  */
 public final class LDIFChangeRecordReader extends AbstractLDIFReader implements ChangeRecordReader {
     private static final Pattern CONTROL_REGEX = Pattern
-            .compile("^\\s*(\\d+(.\\d+)*)(\\s+((true)|(false)))?\\s*(:(:)?\\s*?\\S+)?\\s*$");
+            .compile("^\\s*+(\\d++(\\.\\d++)*+)(\\s++((true)|(false)))?\\s*+(:(:)?\\s*?\\S++)?\\s*+$");
 
     /** Poison used to indicate end of LDIF. */
     private static final ChangeRecord EOF = Requests.newAddRequest(DN.rootDN());
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java b/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java
index afed07c..0b05bc0 100644
--- a/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java
+++ b/opendj-core/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java
@@ -14,6 +14,7 @@
  * Copyright 2011-2016 ForgeRock AS.
  * Portions Copyright 2014 Manuel Gaupp
  * Portions copyright 2016 Matthew Stevenson
+ * Portions Copyrighted 2026 3A Systems, LLC.
  */
 
 package org.forgerock.opendj.ldif;
@@ -1176,6 +1177,100 @@
     }
 
     /**
+     * Test to read a record containing a control with a malformed OID
+     * (non-digit inside an arc). Must throw an error.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class)
+    public void testParseChangeRecordEntryWithMalformedControlOID() throws Exception {
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "dn: ou=Product Development, dc=airius, dc=com",
+                "control: 1a2.3 true :cn",
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: organization",
+                "o: testing"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        reader.readChangeRecord();
+        reader.close();
+    }
+
+    /**
+     * Test to read a record containing a control whose OID has thousands of
+     * arcs. Must parse without StackOverflowError.
+     *
+     * @throws Exception
+     */
+    @Test(timeOut = 10000)
+    public void testParseChangeRecordEntryWithLongControlOID() throws Exception {
+
+        final StringBuilder oid = new StringBuilder("1");
+        for (int i = 0; i < 3000; i++) {
+            oid.append(".1");
+        }
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "dn: ou=Product Development, dc=airius, dc=com",
+                "control: " + oid,
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: organization",
+                "o: testing"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        final ChangeRecord record = reader.readChangeRecord();
+        assertThat(record).isInstanceOf(AddRequest.class);
+        assertThat(record.getControls().get(0).getOID()).isEqualTo(oid.toString());
+        reader.close();
+    }
+
+    /**
+     * Test to read a record containing an invalid control followed by tens of
+     * thousands of spaces. Must throw an error without catastrophic regex
+     * backtracking.
+     *
+     * @throws Exception
+     */
+    @Test(expectedExceptions = DecodeException.class, timeOut = 10000)
+    public void testParseChangeRecordEntryWithControlTrailingWhitespace() throws Exception {
+
+        final StringBuilder control = new StringBuilder("control: 1");
+        for (int i = 0; i < 60000; i++) {
+            control.append(' ');
+        }
+        control.append('x');
+
+        // @formatter:off
+        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+                "dn: ou=Product Development, dc=airius, dc=com",
+                control.toString(),
+                "changetype: add",
+                "objectClass: top",
+                "objectClass: organization",
+                "o: testing"
+        );
+        // @formatter:on
+
+        reader.setSchema(Schema.getDefaultSchema());
+        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
+        // Read the record
+        reader.readChangeRecord();
+        reader.close();
+    }
+
+    /**
      * Test to read an record containing a add control.
      *
      * @throws Exception

--
Gitblit v1.10.0