paragon-review commented on code in PR #13704:
URL: https://github.com/apache/skywalking/pull/13704#discussion_r2811613034


##########
oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/handler/PromQLApiHandler.java:
##########
@@ -859,16 +867,64 @@ private List<Endpoint> 
queryEndpointTraffic(MatcherSetResult parseResult,
             } else if (matcher.getMatchOp() == PromQLParser.NEQ) {
                 endpoints.stream().filter(e -> 
!e.getName().equals(endpointName)).limit(limitNum).forEach(result::add);
             } else if (matcher.getMatchOp() == PromQLParser.RM) {
-                endpoints.stream().filter(e -> 
e.getName().matches(endpointName)).limit(limitNum).forEach(result::add);
+                final Pattern pattern = compileRegex(endpointName);
+                endpoints.stream().filter(e -> safeMatches(pattern, 
e.getName())).limit(limitNum).forEach(result::add);
             } else if (matcher.getMatchOp() == PromQLParser.NRM) {
-                endpoints.stream().filter(e -> 
!e.getName().matches(endpointName)).limit(limitNum).forEach(result::add);
+                final Pattern pattern = compileRegex(endpointName);
+                endpoints.stream().filter(e -> !safeMatches(pattern, 
e.getName())).limit(limitNum).forEach(result::add);
             }
         } else {
             endpoints.stream().limit(limitNum).forEach(result::add);
         }
         return result;
     }
 
+    private static Pattern compileRegex(final String regex) throws 
IllegalExpressionException {
+        try {
+            return Pattern.compile(regex);
+        } catch (PatternSyntaxException e) {
+            throw new IllegalExpressionException("Invalid regex pattern: " + 
e.getMessage(), e);
+        }
+    }
+
+    private static boolean safeMatches(final Pattern pattern, final String 
input) {
+        return pattern.matcher(new TimeLimitedCharSequence(input, 
REGEX_MATCH_TIMEOUT_NANOS)).matches();
+    }
+

Review Comment:
   <!-- paragon-review-issue:ac89b405-ac73-490a-9c8c-35a34804ce07-->
   
   ### Bug: Regex timeout throws bare RuntimeException, bypassing catch blocks
   
   <!-- **HIGH Severity** -->
   
   <!-- DESCRIPTION START -->
   
   Regex timeout throws bare RuntimeException, bypassing catch blocks. Callers 
only catch IllegalExpressionException, so timeouts surface as HTTP 500. Wrap 
RuntimeException inside safeMatches as IllegalExpressionException.
   
   <!-- DESCRIPTION END -->
   
   <details>
   <summary>View Details</summary>
   
   **Location:** 
`oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/handler/PromQLApiHandler.java`
 (lines 893-895)
   
   ### Analysis
   
   **Regex timeout throws bare RuntimeException, bypassing catch blocks**
   
   | | |
   |---|---|
   | **What fails** | When a ReDoS pattern triggers the 5s timeout, 
TimeLimitedCharSequence.charAt throws RuntimeException which is not caught by 
the IllegalExpressionException catch blocks at lines 342 and 403. |
   | **Result** | RuntimeException propagates uncaught past the 
IllegalExpressionException catch blocks, resulting in HTTP 500 with a stack 
trace leaked to the client. |
   | **Expected** | The timeout should be caught and wrapped as 
IllegalExpressionException so it returns a proper error response with 
ResultStatus.ERROR and a user-friendly message. |
   | **Impact** | The entire ReDoS protection goal of this PR is undermined — 
attackers still get a 5s thread hang and an ugly 500 error instead of a 
graceful rejection. |
   
   <details>
   <summary>How to reproduce</summary>
   
   ```bash
   Send a PromQL query with a catastrophic backtracking regex pattern, e.g. 
service_name=~"(a+)+$" against a service list. Wait 5 seconds for the timeout 
to trigger.
   ```
   
   </details>
   
   <details>
   <summary>Patch Details</summary>
   
   ```diff
   -    private static boolean safeMatches(final Pattern pattern, final String 
input) {
   -        return pattern.matcher(new TimeLimitedCharSequence(input, 
REGEX_MATCH_TIMEOUT_NANOS)).matches();
   +    private static boolean safeMatches(final Pattern pattern, final String 
input) throws IllegalExpressionException {
   +        try {
   +            return pattern.matcher(new TimeLimitedCharSequence(input, 
REGEX_MATCH_TIMEOUT_NANOS)).matches();
   +        } catch (RuntimeException e) {
   +            throw new IllegalExpressionException("Regex evaluation timed 
out", e);
   +        }
   ```
   
   </details>
   
   <details>
   <summary>AI Fix Prompt</summary>
   
   ```
   Fix this issue: Regex timeout throws bare RuntimeException, bypassing catch 
blocks. Callers only catch IllegalExpressionException, so timeouts surface as 
HTTP 500. Wrap RuntimeException inside safeMatches as 
IllegalExpressionException.
   
   Location: 
oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/handler/PromQLApiHandler.java
 (lines 893-895)
   Problem: When a ReDoS pattern triggers the 5s timeout, 
TimeLimitedCharSequence.charAt throws RuntimeException which is not caught by 
the IllegalExpressionException catch blocks at lines 342 and 403.
   Current behavior: RuntimeException propagates uncaught past the 
IllegalExpressionException catch blocks, resulting in HTTP 500 with a stack 
trace leaked to the client.
   Expected: The timeout should be caught and wrapped as 
IllegalExpressionException so it returns a proper error response with 
ResultStatus.ERROR and a user-friendly message.
   Steps to reproduce: Send a PromQL query with a catastrophic backtracking 
regex pattern, e.g. service_name=~"(a+)+$" against a service list. Wait 5 
seconds for the timeout to trigger.
   
   Provide a code fix.
   ```
   
   </details>
   
   </details>
   
   ---
   <sub>**Tip:** Reply with `@paragon-run` to automatically fix this issue</sub>
   
   



##########
oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/handler/PromQLApiHandler.java:
##########
@@ -859,16 +867,64 @@ private List<Endpoint> 
queryEndpointTraffic(MatcherSetResult parseResult,
             } else if (matcher.getMatchOp() == PromQLParser.NEQ) {
                 endpoints.stream().filter(e -> 
!e.getName().equals(endpointName)).limit(limitNum).forEach(result::add);
             } else if (matcher.getMatchOp() == PromQLParser.RM) {
-                endpoints.stream().filter(e -> 
e.getName().matches(endpointName)).limit(limitNum).forEach(result::add);
+                final Pattern pattern = compileRegex(endpointName);
+                endpoints.stream().filter(e -> safeMatches(pattern, 
e.getName())).limit(limitNum).forEach(result::add);
             } else if (matcher.getMatchOp() == PromQLParser.NRM) {
-                endpoints.stream().filter(e -> 
!e.getName().matches(endpointName)).limit(limitNum).forEach(result::add);
+                final Pattern pattern = compileRegex(endpointName);
+                endpoints.stream().filter(e -> !safeMatches(pattern, 
e.getName())).limit(limitNum).forEach(result::add);
             }
         } else {
             endpoints.stream().limit(limitNum).forEach(result::add);
         }
         return result;
     }
 
+    private static Pattern compileRegex(final String regex) throws 
IllegalExpressionException {
+        try {
+            return Pattern.compile(regex);
+        } catch (PatternSyntaxException e) {
+            throw new IllegalExpressionException("Invalid regex pattern: " + 
e.getMessage(), e);
+        }
+    }
+
+    private static boolean safeMatches(final Pattern pattern, final String 
input) {
+        return pattern.matcher(new TimeLimitedCharSequence(input, 
REGEX_MATCH_TIMEOUT_NANOS)).matches();
+    }
+
+    private static class TimeLimitedCharSequence implements CharSequence {
+        private final CharSequence delegate;
+        private final long deadlineNanos;
+
+        private TimeLimitedCharSequence(final CharSequence delegate, final 
long timeoutNanos) {
+            this.delegate = delegate;
+            this.deadlineNanos = System.nanoTime() + timeoutNanos;
+        }
+
+        @Override
+        public int length() {
+            return delegate.length();
+        }
+
+        @Override
+        public char charAt(final int index) {
+            if (System.nanoTime() > deadlineNanos) {
+                throw new RuntimeException("Regex matching timed out");
+            }
+            return delegate.charAt(index);
+        }
+
+        @Override
+        public CharSequence subSequence(final int start, final int end) {

Review Comment:
   <!-- paragon-review-issue:120ceb17-7abb-46cd-a503-157c78cb728d-->
   
   ### Bug: subSequence recomputes deadline from remaining time plus a new 
nanoTime call
   
   <!-- **LOW Severity** -->
   
   <!-- DESCRIPTION START -->
   
   subSequence recomputes deadline from remaining time plus a new nanoTime 
call. This drifts the effective deadline forward past the original 5s. Pass the 
absolute deadlineNanos directly to the child instance.
   
   <!-- DESCRIPTION END -->
   
   <details>
   <summary>View Details</summary>
   
   **Location:** 
`oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/handler/PromQLApiHandler.java`
 (lines 917-919)
   
   ### Analysis
   
   **subSequence recomputes deadline from remaining time plus a new nanoTime 
call**
   
   | | |
   |---|---|
   | **What fails** | TimeLimitedCharSequence.subSequence computes remaining 
time then the constructor adds System.nanoTime() again, extending the effective 
deadline beyond the intended 5 seconds on each call. |
   | **Result** | The timeout window silently extends beyond 5 seconds across 
repeated subSequence invocations due to elapsed time between the two 
System.nanoTime() calls. |
   | **Expected** | The absolute deadline should be preserved across all child 
TimeLimitedCharSequence instances without recalculation. |
   | **Impact** | The ReDoS timeout protection is weakened — patterns that 
trigger many subSequence calls can exceed the 5-second budget. |
   
   <details>
   <summary>How to reproduce</summary>
   
   ```bash
   Use a regex that triggers repeated subSequence calls internally (e.g., 
alternation groups). The cumulative drift from each nanoTime() gap extends the 
effective timeout.
   ```
   
   </details>
   
   <details>
   <summary>AI Fix Prompt</summary>
   
   ```
   Fix this issue: subSequence recomputes deadline from remaining time plus a 
new nanoTime call. This drifts the effective deadline forward past the original 
5s. Pass the absolute deadlineNanos directly to the child instance.
   
   Location: 
oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/handler/PromQLApiHandler.java
 (lines 917-919)
   Problem: TimeLimitedCharSequence.subSequence computes remaining time then 
the constructor adds System.nanoTime() again, extending the effective deadline 
beyond the intended 5 seconds on each call.
   Current behavior: The timeout window silently extends beyond 5 seconds 
across repeated subSequence invocations due to elapsed time between the two 
System.nanoTime() calls.
   Expected: The absolute deadline should be preserved across all child 
TimeLimitedCharSequence instances without recalculation.
   Steps to reproduce: Use a regex that triggers repeated subSequence calls 
internally (e.g., alternation groups). The cumulative drift from each 
nanoTime() gap extends the effective timeout.
   
   Provide a code fix.
   ```
   
   </details>
   
   </details>
   
   ---
   <sub>**Tip:** Reply with `@paragon-run` to automatically fix this issue</sub>
   
   



##########
oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/handler/PromQLApiHandler.java:
##########
@@ -859,16 +867,64 @@ private List<Endpoint> 
queryEndpointTraffic(MatcherSetResult parseResult,
             } else if (matcher.getMatchOp() == PromQLParser.NEQ) {
                 endpoints.stream().filter(e -> 
!e.getName().equals(endpointName)).limit(limitNum).forEach(result::add);
             } else if (matcher.getMatchOp() == PromQLParser.RM) {
-                endpoints.stream().filter(e -> 
e.getName().matches(endpointName)).limit(limitNum).forEach(result::add);
+                final Pattern pattern = compileRegex(endpointName);
+                endpoints.stream().filter(e -> safeMatches(pattern, 
e.getName())).limit(limitNum).forEach(result::add);
             } else if (matcher.getMatchOp() == PromQLParser.NRM) {
-                endpoints.stream().filter(e -> 
!e.getName().matches(endpointName)).limit(limitNum).forEach(result::add);
+                final Pattern pattern = compileRegex(endpointName);
+                endpoints.stream().filter(e -> !safeMatches(pattern, 
e.getName())).limit(limitNum).forEach(result::add);
             }
         } else {
             endpoints.stream().limit(limitNum).forEach(result::add);
         }
         return result;
     }
 
+    private static Pattern compileRegex(final String regex) throws 
IllegalExpressionException {

Review Comment:
   <!-- paragon-review-issue:4a25a634-59ad-4933-a402-4c7dd77d2987-->
   
   ### Security: No tests exist for the ReDoS-protection code paths
   
   <!-- **MEDIUM Severity** -->
   
   <!-- DESCRIPTION START -->
   
   No tests exist for the ReDoS-protection code paths. Security-critical code 
lacks validation. Add tests for compileRegex, safeMatches, and 
TimeLimitedCharSequence.
   
   <!-- DESCRIPTION END -->
   
   <details>
   <summary>View Details</summary>
   
   **Location:** 
`oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/handler/PromQLApiHandler.java`
 (lines 882-930)
   
   ### Analysis
   
   **No tests exist for the ReDoS-protection code paths. Security-critical code 
lacks validation**
   
   | | |
   |---|---|
   | **What fails** | There are zero tests covering TimeLimitedCharSequence, 
compileRegex, or safeMatches — all newly added security-critical code for ReDoS 
protection. |
   | **Result** | No test coverage for the ReDoS protection mechanism, meaning 
regressions or bugs (like finding #1) go undetected. |
   | **Expected** | Tests should cover: invalid regex → 
IllegalExpressionException, catastrophic backtracking → timeout within ~5s, 
normal pattern → correct match/no-match, subSequence preserving timeout. |
   | **Impact** | Without tests, the ReDoS protection can silently break. 
Combined with finding #1 (uncaught RuntimeException), this means the core 
security improvement of this PR is unvalidated. |
   
   <details>
   <summary>How to reproduce</summary>
   
   ```bash
   Search the test directory for any test referencing TimeLimitedCharSequence, 
compileRegex, or safeMatches. None exist.
   ```
   
   </details>
   
   <details>
   <summary>AI Fix Prompt</summary>
   
   ```
   Fix this issue: No tests exist for the ReDoS-protection code paths. 
Security-critical code lacks validation. Add tests for compileRegex, 
safeMatches, and TimeLimitedCharSequence.
   
   Location: 
oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/handler/PromQLApiHandler.java
 (lines 882-930)
   Problem: There are zero tests covering TimeLimitedCharSequence, 
compileRegex, or safeMatches — all newly added security-critical code for ReDoS 
protection.
   Current behavior: No test coverage for the ReDoS protection mechanism, 
meaning regressions or bugs (like finding #1) go undetected.
   Expected: Tests should cover: invalid regex → IllegalExpressionException, 
catastrophic backtracking → timeout within ~5s, normal pattern → correct 
match/no-match, subSequence preserving timeout.
   Steps to reproduce: Search the test directory for any test referencing 
TimeLimitedCharSequence, compileRegex, or safeMatches. None exist.
   
   Provide a code fix.
   ```
   
   </details>
   
   </details>
   
   ---
   <sub>**Tip:** Reply with `@paragon-run` to automatically fix this issue</sub>
   
   



##########
oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/handler/PromQLApiHandler.java:
##########
@@ -859,16 +867,64 @@ private List<Endpoint> 
queryEndpointTraffic(MatcherSetResult parseResult,
             } else if (matcher.getMatchOp() == PromQLParser.NEQ) {
                 endpoints.stream().filter(e -> 
!e.getName().equals(endpointName)).limit(limitNum).forEach(result::add);
             } else if (matcher.getMatchOp() == PromQLParser.RM) {
-                endpoints.stream().filter(e -> 
e.getName().matches(endpointName)).limit(limitNum).forEach(result::add);
+                final Pattern pattern = compileRegex(endpointName);
+                endpoints.stream().filter(e -> safeMatches(pattern, 
e.getName())).limit(limitNum).forEach(result::add);
             } else if (matcher.getMatchOp() == PromQLParser.NRM) {
-                endpoints.stream().filter(e -> 
!e.getName().matches(endpointName)).limit(limitNum).forEach(result::add);
+                final Pattern pattern = compileRegex(endpointName);
+                endpoints.stream().filter(e -> !safeMatches(pattern, 
e.getName())).limit(limitNum).forEach(result::add);
             }
         } else {
             endpoints.stream().limit(limitNum).forEach(result::add);
         }
         return result;
     }
 
+    private static Pattern compileRegex(final String regex) throws 
IllegalExpressionException {
+        try {
+            return Pattern.compile(regex);
+        } catch (PatternSyntaxException e) {
+            throw new IllegalExpressionException("Invalid regex pattern: " + 
e.getMessage(), e);
+        }
+    }
+
+    private static boolean safeMatches(final Pattern pattern, final String 
input) {
+        return pattern.matcher(new TimeLimitedCharSequence(input, 
REGEX_MATCH_TIMEOUT_NANOS)).matches();
+    }
+
+    private static class TimeLimitedCharSequence implements CharSequence {
+        private final CharSequence delegate;
+        private final long deadlineNanos;
+
+        private TimeLimitedCharSequence(final CharSequence delegate, final 
long timeoutNanos) {
+            this.delegate = delegate;
+            this.deadlineNanos = System.nanoTime() + timeoutNanos;
+        }
+
+        @Override
+        public int length() {
+            return delegate.length();
+        }
+
+        @Override
+        public char charAt(final int index) {
+            if (System.nanoTime() > deadlineNanos) {

Review Comment:
   <!-- paragon-review-issue:1f426f8d-5330-4d01-80a0-8bb4f41c009d-->
   
   ### Bug: nanoTime comparison uses > operator instead of subtraction
   
   <!-- **LOW Severity** -->
   
   <!-- DESCRIPTION START -->
   
   nanoTime comparison uses > operator instead of subtraction. Long overflow 
can cause incorrect deadline checks. Use System.nanoTime() - deadlineNanos >= 0 
instead.
   
   <!-- DESCRIPTION END -->
   
   <details>
   <summary>View Details</summary>
   
   **Location:** 
`oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/handler/PromQLApiHandler.java`
 (lines 910)
   
   ### Analysis
   
   **nanoTime comparison uses > operator instead of subtraction**
   
   | | |
   |---|---|
   | **What fails** | The charAt method compares System.nanoTime() > 
deadlineNanos directly, which is technically incorrect for nanoTime values that 
overflow across the long boundary. |
   | **Result** | Direct comparison may produce incorrect results if nanoTime 
wraps around the long overflow boundary. |
   | **Expected** | Should use the subtraction idiom (System.nanoTime() - 
deadlineNanos >= 0) as used by JDK's own AbstractQueuedSynchronizer for 
correctness. |
   | **Impact** | Practically zero risk for a 5s window, but violates the 
JDK-standard idiom for safe nanoTime comparison. |
   
   <details>
   <summary>How to reproduce</summary>
   
   ```bash
   This is a theoretical issue that would only manifest after ~292 years of JVM 
uptime when nanoTime overflows, but the subtraction idiom is the JDK-standard 
pattern.
   ```
   
   </details>
   
   <details>
   <summary>Patch Details</summary>
   
   ```diff
   -            if (System.nanoTime() > deadlineNanos) {
   +            if (System.nanoTime() - deadlineNanos >= 0) {
   ```
   
   </details>
   
   <details>
   <summary>AI Fix Prompt</summary>
   
   ```
   Fix this issue: nanoTime comparison uses > operator instead of subtraction. 
Long overflow can cause incorrect deadline checks. Use System.nanoTime() - 
deadlineNanos >= 0 instead.
   
   Location: 
oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/handler/PromQLApiHandler.java
 (lines 910)
   Problem: The charAt method compares System.nanoTime() > deadlineNanos 
directly, which is technically incorrect for nanoTime values that overflow 
across the long boundary.
   Current behavior: Direct comparison may produce incorrect results if 
nanoTime wraps around the long overflow boundary.
   Expected: Should use the subtraction idiom (System.nanoTime() - 
deadlineNanos >= 0) as used by JDK's own AbstractQueuedSynchronizer for 
correctness.
   Steps to reproduce: This is a theoretical issue that would only manifest 
after ~292 years of JVM uptime when nanoTime overflows, but the subtraction 
idiom is the JDK-standard pattern.
   
   Provide a code fix.
   ```
   
   </details>
   
   </details>
   
   ---
   <sub>**Tip:** Reply with `@paragon-run` to automatically fix this issue</sub>
   
   



-- 
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