You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opennlp.apache.org by rz...@apache.org on 2026/09/05 17:11:09 UTC

(opennlp) branch main updated: OPENNLP-1925: Document RegEx exemption and harden ReDoS guard test (#1268)

This is an automated email from the ASF dual-hosted git repository.

rzo1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opennlp.git


The following commit(s) were added to refs/heads/main by this push:
     new b81a2a7d1 OPENNLP-1925: Document RegEx exemption and harden ReDoS guard test (#1268)
b81a2a7d1 is described below

commit b81a2a7d1ea6c94fe58689cb0c84e93e24ee229e
Author: Kristian Rickert <kr...@gmail.com>
AuthorDate: Sat Sep 5 13:11:02 2026 -0400

    OPENNLP-1925: Document RegEx exemption and harden ReDoS guard test (#1268)
---
 .../opennlp/tools/namefind/RegexNameFinder.java    |  3 ++
 .../tools/namefind/RegexNameFinderFactory.java     |  3 ++
 .../tools/namefind/RegexNameFinderFactoryTest.java | 47 ++++++++++++++--------
 3 files changed, 36 insertions(+), 17 deletions(-)

diff --git a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinder.java b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinder.java
index b8239c28e..c62841ebd 100644
--- a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinder.java
+++ b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinder.java
@@ -29,6 +29,9 @@ import opennlp.tools.util.Span;
 
 /**
  * A {@link TokenNameFinder} implementation based on a series of regular expressions.
+ * <p>
+ * Please note: RegEx is not allowed in the processing path of OpenNLP.
+ * However, a RegEx step is an obvious exception.
  */
 public final class RegexNameFinder implements TokenNameFinder {
 
diff --git a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinderFactory.java b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinderFactory.java
index 0890ae051..003d94512 100644
--- a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinderFactory.java
+++ b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/namefind/RegexNameFinderFactory.java
@@ -26,6 +26,9 @@ import java.util.regex.Pattern;
  *
  * Returns a {@link RegexNameFinder} based on a selection of
  * defaults or a configuration and a selection of defaults.
+ * <p>
+ * Please note: RegEx is not allowed in the processing path of OpenNLP.
+ * However, a RegEx step is an obvious exception.
  */
 public class RegexNameFinderFactory {
 
diff --git a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/namefind/RegexNameFinderFactoryTest.java b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/namefind/RegexNameFinderFactoryTest.java
index 6fdea6414..9bc72dbb2 100644
--- a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/namefind/RegexNameFinderFactoryTest.java
+++ b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/namefind/RegexNameFinderFactoryTest.java
@@ -20,10 +20,15 @@ package opennlp.tools.namefind;
 import java.time.Duration;
 import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Stream;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Named;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 import opennlp.tools.tokenize.WhitespaceTokenizer;
 import opennlp.tools.util.Span;
@@ -95,24 +100,32 @@ public class RegexNameFinderFactoryTest {
    * Crafted inputs that used to drive the built-in EMAIL and URL patterns into
    * catastrophic backtracking / deep recursion (ReDoS, CWE-1333 / CWE-400 / CWE-674).
    * The hardened patterns must finish quickly regardless of input length.
+   * <p>
+   * Each attack runs as its own test so a regression names the exact input that
+   * got slow. Each case does one warmup run outside the timed section, so the
+   * budget measures the match itself rather than JVM cold start on throttled
+   * machines.
    */
-  @Test
-  void testBuiltinPatternsAreNotVulnerableToReDoS() {
-    final String emailLocalBlowup = "a".repeat(100_000) + "@ ";
-    final String emailDomainBlowup = "x@a" + "-a".repeat(60_000) + " ";
-    final String urlPathRecursion = "http://a.com/" + "a".repeat(100_000) + " ";
-    final String urlNestedBlowup = "http://a.com" + "/a".repeat(50_000) + "%z";
-    // The reported StackOverflowError repro: a long ?a&a&a&... query string, which
-    // drove the nested (&(...)+ ... )* group in the old URL pattern into deep recursion.
-    final String urlQueryRecursion = "http://a.com/p?a" + "&a".repeat(50_000) + "= ";
-
-    Assertions.assertTimeoutPreemptively(Duration.ofSeconds(2), () -> {
-      for (String attack : new String[] {
-          emailLocalBlowup, emailDomainBlowup, urlPathRecursion, urlNestedBlowup, urlQueryRecursion}) {
-        String[] tokens = WhitespaceTokenizer.INSTANCE.tokenize(attack);
-        regexNameFinder.find(tokens);
-      }
-    });
+  private static Stream<Arguments> reDoSAttacks() {
+    return Stream.of(
+        Arguments.of(Named.of("emailLocalBlowup", "a".repeat(100_000) + "@ ")),
+        Arguments.of(Named.of("emailDomainBlowup", "x@a" + "-a".repeat(60_000) + " ")),
+        Arguments.of(Named.of("urlPathRecursion", "http://a.com/" + "a".repeat(100_000) + " ")),
+        Arguments.of(Named.of("urlNestedBlowup", "http://a.com" + "/a".repeat(50_000) + "%z")),
+        // The reported StackOverflowError repro: a long ?a&a&a&... query string, which
+        // drove the nested (&(...)+ ... )* group in the old URL pattern into deep recursion.
+        Arguments.of(Named.of("urlQueryRecursion", "http://a.com/p?a" + "&a".repeat(50_000) + "= ")));
+  }
+
+  @ParameterizedTest(name = "{0}")
+  @MethodSource("reDoSAttacks")
+  void testBuiltinPatternsAreNotVulnerableToReDoS(String attack) {
+    String[] tokens = WhitespaceTokenizer.INSTANCE.tokenize(attack);
+    // Warmup: JIT-compile the pattern engine before the timed run.
+    regexNameFinder.find(tokens);
+
+    Assertions.assertTimeoutPreemptively(Duration.ofSeconds(2),
+        () -> regexNameFinder.find(tokens));
   }
 
   /**