Copilot commented on code in PR #414:
URL: https://github.com/apache/commons-jexl/pull/414#discussion_r3893665187


##########
src/main/java/org/apache/commons/jexl3/internal/Interpreter.java:
##########
@@ -1377,12 +1378,37 @@ protected Object visit(final ASTEQSNode node, final 
Object data) {
     @Override
     protected Object visit(final ASTERNode node, final Object data) {
         final Object left = node.jjtGetChild(0).jjtAccept(this, data);
-        final Object right = node.jjtGetChild(1).jjtAccept(this, data);
+        final JexlNode rightNode = node.jjtGetChild(1);
+        final Object right = resolvePattern(rightNode, 
rightNode.jjtAccept(this, data));
         // note the arguments inversion between 'in'/'matches' and 'contains'
         // if x in y then y contains x
         return operators.contains(node, JexlOperator.CONTAINS, right, left);
     }
 
+    /**
+     * If the right operand of {@code =~} / {@code !~} is a string literal, 
compile it to a Pattern once and
+     * cache the result in the node's value slot (same mechanism as negated 
numeric literals).
+     * Dynamic string values (from variables) are returned unchanged.
+     * The regex string length is validated before compilation (matches 
JexlArithmetic.REGEX_PATTERN_MAX_LENGTH).
+     */
+    private static Object resolvePattern(final JexlNode rightNode, final 
Object right) {
+        if (right instanceof CharSequence && rightNode instanceof 
JexlNode.Constant) {
+            final Object cached = rightNode.jjtGetValue();
+            if (cached instanceof Pattern) {
+                return cached;
+            }
+            final String regex = right.toString();
+            final int maxLen = 2048;

Review Comment:
   The regex max-length is duplicated as a magic number here, while 
`JexlArithmetic` defines `REGEX_PATTERN_MAX_LENGTH`. This creates a risk of 
divergence (and the PR description explicitly claims they match). Consider 
centralizing the limit by making the constant accessible from `internal` (e.g., 
`public static final` on a shared class, or a small utility method) and 
referencing it here.



##########
src/main/java/org/apache/commons/jexl3/internal/Interpreter.java:
##########
@@ -1377,12 +1378,37 @@ protected Object visit(final ASTEQSNode node, final 
Object data) {
     @Override
     protected Object visit(final ASTERNode node, final Object data) {
         final Object left = node.jjtGetChild(0).jjtAccept(this, data);
-        final Object right = node.jjtGetChild(1).jjtAccept(this, data);
+        final JexlNode rightNode = node.jjtGetChild(1);
+        final Object right = resolvePattern(rightNode, 
rightNode.jjtAccept(this, data));
         // note the arguments inversion between 'in'/'matches' and 'contains'
         // if x in y then y contains x
         return operators.contains(node, JexlOperator.CONTAINS, right, left);
     }
 
+    /**
+     * If the right operand of {@code =~} / {@code !~} is a string literal, 
compile it to a Pattern once and
+     * cache the result in the node's value slot (same mechanism as negated 
numeric literals).
+     * Dynamic string values (from variables) are returned unchanged.
+     * The regex string length is validated before compilation (matches 
JexlArithmetic.REGEX_PATTERN_MAX_LENGTH).
+     */
+    private static Object resolvePattern(final JexlNode rightNode, final 
Object right) {
+        if (right instanceof CharSequence && rightNode instanceof 
JexlNode.Constant) {
+            final Object cached = rightNode.jjtGetValue();
+            if (cached instanceof Pattern) {
+                return cached;
+            }
+            final String regex = right.toString();
+            final int maxLen = 2048;
+            if (regex.length() > maxLen) {
+                throw new ArithmeticException("regular expression too long: " 
+ regex.length() + " > " + maxLen);
+            }
+            final Pattern compiled = Pattern.compile(regex);
+            rightNode.jjtSetValue(compiled);
+            return compiled;
+        }
+        return right;
+    }

Review Comment:
   Caching the compiled Pattern by mutating the AST node (`jjtSetValue`) 
introduces shared mutable state in the script tree. If the same parsed 
script/AST is executed concurrently, this is a data race (even if benign most 
of the time) and can violate expected thread-safety guarantees. A safer 
approach is to cache per-script (immutable structure created at parse/compile 
time) or per-interpreter execution (e.g., an Interpreter-local map keyed by 
node identity). If you keep node-level caching, consider making it thread-safe 
(e.g., safe publication/atomic install) and document the thread-safety 
implications.



##########
src/main/java/org/apache/commons/jexl3/JexlArithmetic.java:
##########
@@ -1374,9 +1422,13 @@ public Object multiply(final Object left, final Object 
right) {
             final double r = toDouble(strictCast, right);
             return l * r;
         }
-        // otherwise treat as BigInteger
+        // otherwise treat as BigInteger; pre-check bit-length sum to avoid 
O(n²) on huge operands
         final BigInteger l = toBigInteger(strictCast, left);
         final BigInteger r = toBigInteger(strictCast, right);
+        final int precision = getMathContext().getPrecision();
+        if (precision > 0 && l.bitLength() + r.bitLength() > precision * 10 / 
3 + 1) {
+            throw new ArithmeticException("BigInteger precision exceeded");

Review Comment:
   This newly introduced exception message is much less actionable than 
`checkBigIntegerPrecision(...)` (which includes bit-length and context 
precision). Since this is a security/operational hardening boundary, having 
consistent, contextual error messages helps debugging and support. Consider 
including the computed limit and observed operand bit-lengths (or delegating to 
the same formatter used elsewhere) while keeping sensitive data exposure in 
mind.



##########
src/main/java/org/apache/commons/jexl3/parser/NumberParser.java:
##########
@@ -32,6 +34,30 @@ public final class NumberParser implements Serializable {
      */
     private static final long serialVersionUID = 1L;
 
+    /**
+     * Hard upper bound on BigInteger literal digits when no engine precision 
is configured.
+     * Acts as a parse-time DoS guard independent of any MathContext 
(JEXL-security f014).
+     */
+    static final int MAX_BIGINTEGER_DIGITS = 256;
+
+    /**
+     * Returns the maximum digit count allowed for a BigInteger literal.
+     * When a JEXL engine with a bounded MathContext is active on the current 
thread, the
+     * engine's precision (in decimal digits) is used as the limit; otherwise 
MAX_BIGINTEGER_DIGITS applies.
+     * At runtime, JexlArithmetic.checkBigIntegerPrecision() enforces a 
stricter bit-length limit
+     * based on the same precision (f014, f013).
+     */
+    private static int maxBigIntegerDigits() {
+        final JexlEngine engine = JexlEngine.getThreadEngine();
+        if (engine != null) {
+            final int precision = 
engine.getArithmetic().getMathContext().getPrecision();
+            if (precision > 0) {
+                return precision;
+            }
+        }
+        return MAX_BIGINTEGER_DIGITS;
+    }

Review Comment:
   The doc states the engine precision is interpreted as a limit \"in decimal 
digits\", but the subsequent checks compare that limit to the literal length in 
whatever base is being parsed (e.g., hex/octal digit counts). That makes the 
enforced limit inconsistent with the documented unit (decimal digits) and can 
reject valid literals earlier than intended (or accept more than intended) 
depending on base. Consider converting the precision-based limit to an 
equivalent max digit-count for the current base (using a log-based conversion) 
or enforce a base-independent bound (e.g., max bits) consistently at parse time.



##########
src/test/java/org/apache/commons/jexl3/ArithmeticTest.java:
##########
@@ -2409,4 +2409,75 @@ void setOptions(final JexlOptions options) {
         assertEquals("zero", jexl.createExpression("array.0").evaluate(jc));
         assertEquals("one", jexl.createExpression("array.1").evaluate(jc));
     }
+
+    // ----- security fixes f012 / f013 / f014 -----
+
+    /**
+     * f014: BigInteger literal with more digits than MAX_BIGINTEGER_DIGITS 
must be rejected at parse time.
+     */
+    @Test
+    void testBigIntegerLiteralTooLong() {
+        // normal H-literal still works
+        assertNotNull(JEXL.createScript("42H"));
+        // a literal just over the cap (256 + 1 digits + 'H') must fail to 
parse
+        final char[] digits = new char[256 + 1];
+        java.util.Arrays.fill(digits, '1');
+        final String huge = new String(digits) + "H";
+        assertThrows(JexlException.Parsing.class, () -> 
JEXL.createScript(huge));
+    }
+
+    /**
+     * f013: BigInteger arithmetic results that exceed the MathContext 
precision must throw.
+     */
+    @Test
+    void testBigIntegerArithmeticPrecisionCap() {
+        // precision=3 caps at ~11 bits (formula: 3 * 10 / 3 + 1 = 11), so 
results > 2047 are rejected
+        final JexlArithmetic bounded = new JexlArithmetic(true, new 
MathContext(3), JexlArithmetic.BIGD_SCALE);
+        final JexlEngine jexl = new JexlBuilder().arithmetic(bounded).create();
+        // small values are fine
+        assertEquals(new BigInteger("3"), jexl.createScript("a + b", "a", "b")
+                .execute(null, BigInteger.ONE, BigInteger.valueOf(2L)));
+        // result > 2047 is rejected: 1500 + 1000 = 2500, bitLength=12 > 11
+        assertThrows(JexlException.class, () ->
+                jexl.createScript("a + b", "a", "b")
+                    .execute(null, BigInteger.valueOf(1500L), 
BigInteger.valueOf(1000L)));
+        // multiply pre-check: 64 (7 bits) * 64 (7 bits), sum of operand bits 
= 14 > 11
+        assertThrows(JexlException.class, () ->
+                jexl.createScript("a * b", "a", "b")
+                    .execute(null, BigInteger.valueOf(64L), 
BigInteger.valueOf(64L)));
+    }
+
+    /**
+     * f012: a regex pattern string longer than REGEX_PATTERN_MAX_LENGTH must 
throw.
+     */
+    @Test
+    void testRegexPatternTooLong() {
+        final JexlEngine jexl = new JexlBuilder().strict(true).create();
+        final JexlScript script = jexl.createScript("x =~ y", "x", "y");
+        final char[] chars = new char[JexlArithmetic.REGEX_PATTERN_MAX_LENGTH 
+ 1];
+        java.util.Arrays.fill(chars, 'a');
+        final String longPattern = new String(chars);
+        assertThrows(JexlException.class, () -> script.execute(null, "abc", 
longPattern));
+    }
+
+    /**
+     * f012: regex matching must respond to thread interruption so a 
catastrophic-backtracking
+     * pattern does not hang a cancellable engine indefinitely.
+     */
+    @Test
+    void testRegexMatchingInterruptible() {
+        final JexlEngine jexl = new JexlBuilder().cancellable(true).create();
+        // Classic catastrophic-backtracking: (a+)+b against a non-matching 
string
+        final JexlScript script = jexl.createScript("x =~ y", "x", "y");
+        final String evilPattern = "(a+)+b";
+        final char[] chars = new char[20];
+        java.util.Arrays.fill(chars, 'a');
+        final String evilValue = new String(chars) + "c";
+        try {
+            Thread.currentThread().interrupt();
+            assertThrows(JexlException.Cancel.class, () -> 
script.execute(null, evilValue, evilPattern));
+        } finally {
+            Thread.interrupted(); // clear so it does not leak into other tests
+        }
+    }

Review Comment:
   This test interrupts the thread before execution starts, so it can pass even 
if the regex engine never checks for interruption during matching (e.g., if 
cancellation is detected earlier in the evaluation pipeline). To specifically 
validate the new `InterruptibleCharSequence` behavior, add a test that starts 
the regex match in a separate thread, waits until it is actively running, then 
interrupts that thread and asserts it terminates promptly with 
`JexlException.Cancel`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to