This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 7cefcffaac TODO-238 — BUG × juneau-rest-server fixes: BUG-09
request-derived leaks (CachingHttpServletRequest/UrlPath) + mutable-state
exposure (RestMatcherList, Mixin, RestContext), BUG-15 auth null-guards;
RestOpContext matcher immutable views + compareTo declaring-class tie-breaker.
Public matcher getters X[]→List (10.0.0).
7cefcffaac is described below
commit 7cefcffaac14bd32a4374d32a94ecf7ebd464dac
Author: James Bognar <[email protected]>
AuthorDate: Fri Jul 17 15:57:45 2026 -0400
TODO-238 — BUG × juneau-rest-server fixes: BUG-09 request-derived leaks
(CachingHttpServletRequest/UrlPath) + mutable-state exposure (RestMatcherList,
Mixin, RestContext), BUG-15 auth null-guards; RestOpContext matcher immutable
views + compareTo declaring-class tie-breaker. Public matcher getters X[]→List
(10.0.0).
Co-authored-by: Cursor <[email protected]>
---
.../apache/juneau/rest/server/MixinAnnotation.java | 28 +++++++--------
.../org/apache/juneau/rest/server/RestContext.java | 4 +--
.../apache/juneau/rest/server/RestOpContext.java | 42 +++++++++++++++-------
.../juneau/rest/server/auth/AuthFilterChain.java | 11 ++++++
.../server/auth/AuthenticatedRequestWrapper.java | 12 ++++++-
.../rest/server/matcher/RestMatcherList.java | 4 +--
.../server/util/CachingHttpServletRequest.java | 4 +--
.../apache/juneau/rest/server/util/UrlPath.java | 4 +--
8 files changed, 74 insertions(+), 35 deletions(-)
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/MixinAnnotation.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/MixinAnnotation.java
index 60c559adaf..80257a565d 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/MixinAnnotation.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/MixinAnnotation.java
@@ -379,32 +379,32 @@ public class MixinAnnotation {
}
@Override /* Overridden from Mixin */ public Class<?> type() {
return type; }
- @Override /* Overridden from Mixin */ public Class<? extends
RestGuard>[] guards() { return guards; }
+ @Override /* Overridden from Mixin */ public Class<? extends
RestGuard>[] guards() { return copyOf(guards); }
@Override /* Overridden from Mixin */ public String roleGuard()
{ return roleGuard; }
@Override /* Overridden from Mixin */ public String
rolesDeclared() { return rolesDeclared; }
- @Override /* Overridden from Mixin */ public Class<? extends
RestConverter>[] converters() { return converters; }
- @Override /* Overridden from Mixin */ public Class<? extends
Encoder>[] encoders() { return encoders; }
- @Override /* Overridden from Mixin */ public Class<? extends
Serializer>[] serializers() { return serializers; }
- @Override /* Overridden from Mixin */ public Class<?>[]
parsers() { return parsers; }
- @Override /* Overridden from Mixin */ public Class<? extends
ResponseProcessor>[] responseProcessors() { return responseProcessors; }
- @Override /* Overridden from Mixin */ public Class<? extends
RestOpArg>[] restOpArgs() { return restOpArgs; }
+ @Override /* Overridden from Mixin */ public Class<? extends
RestConverter>[] converters() { return copyOf(converters); }
+ @Override /* Overridden from Mixin */ public Class<? extends
Encoder>[] encoders() { return copyOf(encoders); }
+ @Override /* Overridden from Mixin */ public Class<? extends
Serializer>[] serializers() { return copyOf(serializers); }
+ @Override /* Overridden from Mixin */ public Class<?>[]
parsers() { return copyOf(parsers); }
+ @Override /* Overridden from Mixin */ public Class<? extends
ResponseProcessor>[] responseProcessors() { return copyOf(responseProcessors); }
+ @Override /* Overridden from Mixin */ public Class<? extends
RestOpArg>[] restOpArgs() { return copyOf(restOpArgs); }
@Override /* Overridden from Mixin */ public Class<? extends
CallLogger> callLogger() { return callLogger; }
@Override /* Overridden from Mixin */ public Class<? extends
HttpPartSerializer> partSerializer() { return partSerializer; }
@Override /* Overridden from Mixin */ public Class<? extends
HttpPartParser> partParser() { return partParser; }
@Override /* Overridden from Mixin */ public Debug debug() {
return debug; }
@Override /* Overridden from Mixin */ public String messages()
{ return messages; }
- @Override /* Overridden from Mixin */ public String[]
defaultRequestHeaders() { return defaultRequestHeaders; }
- @Override /* Overridden from Mixin */ public String[]
defaultResponseHeaders() { return defaultResponseHeaders; }
- @Override /* Overridden from Mixin */ public String[]
defaultRequestAttributes() { return defaultRequestAttributes; }
- @Override /* Overridden from Mixin */ public String[]
produces() { return produces; }
- @Override /* Overridden from Mixin */ public String[]
consumes() { return consumes; }
+ @Override /* Overridden from Mixin */ public String[]
defaultRequestHeaders() { return copyOf(defaultRequestHeaders); }
+ @Override /* Overridden from Mixin */ public String[]
defaultResponseHeaders() { return copyOf(defaultResponseHeaders); }
+ @Override /* Overridden from Mixin */ public String[]
defaultRequestAttributes() { return copyOf(defaultRequestAttributes); }
+ @Override /* Overridden from Mixin */ public String[]
produces() { return copyOf(produces); }
+ @Override /* Overridden from Mixin */ public String[]
consumes() { return copyOf(consumes); }
@Override /* Overridden from Mixin */ public String
defaultAccept() { return defaultAccept; }
@Override /* Overridden from Mixin */ public String
defaultContentType() { return defaultContentType; }
@Override /* Overridden from Mixin */ public String
defaultCharset() { return defaultCharset; }
@Override /* Overridden from Mixin */ public String maxInput()
{ return maxInput; }
@Override /* Overridden from Mixin */ public String path() {
return path; }
- @Override /* Overridden from Mixin */ public String[] paths() {
return paths; }
- @Override /* Overridden from Mixin */ public String[]
noInherit() { return noInherit; }
+ @Override /* Overridden from Mixin */ public String[] paths() {
return copyOf(paths); }
+ @Override /* Overridden from Mixin */ public String[]
noInherit() { return copyOf(noInherit); }
}
/**
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java
index ed42323d40..d1eb9ce4ec 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestContext.java
@@ -4003,7 +4003,7 @@ public class RestContext extends Context {
* @return The resolved mount paths (never {@code null}; possibly
empty).
* @since 10.0.0
*/
- public String[] getPaths() { return paths; }
+ public String[] getPaths() { return copyOf(paths); }
/**
* Returns the path matcher for this context.
@@ -4081,7 +4081,7 @@ public class RestContext extends Context {
* The REST-op-arg classes for this resource.
* <br>Never <jk>null</jk>.
*/
- public Class<? extends RestOpArg>[] getRestOpArgs() { return
restOpArgs.get(); }
+ public Class<? extends RestOpArg>[] getRestOpArgs() { return
copyOf(restOpArgs.get()); }
/**
* Returns the child resources associated with this servlet.
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestOpContext.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestOpContext.java
index c75e665faa..f9cc333e21 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestOpContext.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/RestOpContext.java
@@ -969,6 +969,13 @@ public class RestOpContext extends Context implements
Comparable<RestOpContext>
/** The required matchers extracted from {@link #matchersList}. */
private final Memoizer<RestMatcher[]> requiredMatchers = memoizer(() ->
matchersList.get().getRequiredEntries());
+ // Immutable, cached views over the routing arrays returned by the
public getters. Built once (memoized) so
+ // there is zero per-call allocation on the routing hot path and no
live-array exposure to callers. Internal
+ // hot-path code reads the private array memoizers directly; these
views exist only for the public API surface.
+ private final Memoizer<List<UrlPathMatcher>> pathMatchersView =
memoizer(() -> List.of(pathMatchers.get()));
+ private final Memoizer<List<RestMatcher>> optionalMatchersView =
memoizer(() -> List.of(optionalMatchers.get()));
+ private final Memoizer<List<RestMatcher>> requiredMatchersView =
memoizer(() -> List.of(requiredMatchers.get()));
+
/** The computed response metadata for this operation method. */
private final Memoizer<ResponseBeanMeta> responseMeta = memoizer(() ->
ResponseBeanMeta.create(methodInfo(), appliedAnnotations()));
@@ -1368,7 +1375,7 @@ public class RestOpContext extends Context implements
Comparable<RestOpContext>
// Pre-warm httpMethod so it is in the memoizer cache for
immediate use by compareTo/match.
httpMethod.get();
- var pm = getPathMatchers();
+ var pm = pathMatchers.get();
bs.add(UrlPathMatcher[].class, pm);
bs.addBean(UrlPathMatcher.class, pm.length > 0 ? pm[0]
: null);
@@ -1385,8 +1392,8 @@ public class RestOpContext extends Context implements
Comparable<RestOpContext>
public int compareTo(RestOpContext o) {
int c;
- var pm = getPathMatchers();
- var opm = o.getPathMatchers();
+ var pm = pathMatchers.get();
+ var opm = o.pathMatchers.get();
for (int i = 0; i < Math.min(pm.length, opm.length); i++) {
c = pm[i].compareTo(opm[i]);
if (c != 0)
@@ -1397,11 +1404,11 @@ public class RestOpContext extends Context implements
Comparable<RestOpContext>
if (c != 0)
return c;
- c = cmp(o.getRequiredMatchers().length,
getRequiredMatchers().length);
+ c = cmp(o.requiredMatchers.get().length,
requiredMatchers.get().length);
if (c != 0)
return c;
- c = cmp(o.getOptionalMatchers().length,
getOptionalMatchers().length);
+ c = cmp(o.optionalMatchers.get().length,
optionalMatchers.get().length);
if (c != 0)
return c;
@@ -1428,6 +1435,14 @@ public class RestOpContext extends Context implements
Comparable<RestOpContext>
if (c != 0)
return c;
+ // Declaring-class tie-breaker: keeps compareTo consistent with
equals()/hashCode() (which key off method
+ // identity) for two ops whose methods share the same signature
but differ by declaring class. Only affects
+ // ordering that was previously a zero-tie, so it makes
tie-break stability deterministic rather than changing
+ // any non-tied dispatch precedence.
+ c = cmp(method.getDeclaringClass().getName(),
o.method.getDeclaringClass().getName());
+ if (c != 0)
+ return c;
+
return 0;
}
@@ -1671,31 +1686,34 @@ public class RestOpContext extends Context implements
Comparable<RestOpContext>
*
* @return The path pattern.
*/
- public String getPathPattern() { return
getPathMatchers()[0].toString(); }
+ public String getPathPattern() { return
pathMatchers.get()[0].toString(); }
/**
* Returns the URL path matchers for this operation.
*
* @return The URL path matchers for this operation.
* <br>Never <jk>null</jk>.
+ * <br>The returned list is an immutable, cached view.
*/
- public UrlPathMatcher[] getPathMatchers() { return pathMatchers.get(); }
+ public List<UrlPathMatcher> getPathMatchers() { return
pathMatchersView.get(); }
/**
* Returns the optional matchers for this operation.
*
* @return The optional matchers for this operation.
* <br>Never <jk>null</jk>.
+ * <br>The returned list is an immutable, cached view.
*/
- public RestMatcher[] getOptionalMatchers() { return
optionalMatchers.get(); }
+ public List<RestMatcher> getOptionalMatchers() { return
optionalMatchersView.get(); }
/**
* Returns the required matchers for this operation.
*
* @return The required matchers for this operation.
* <br>Never <jk>null</jk>.
+ * <br>The returned list is an immutable, cached view.
*/
- public RestMatcher[] getRequiredMatchers() { return
requiredMatchers.get(); }
+ public List<RestMatcher> getRequiredMatchers() { return
requiredMatchersView.get(); }
/**
* Returns the call logger for this operation.
@@ -1803,7 +1821,7 @@ public class RestOpContext extends Context implements
Comparable<RestOpContext>
private UrlPathMatch matchPattern(RestSession call) {
UrlPathMatch pm = null;
- for (var pp : getPathMatchers())
+ for (var pp : pathMatchers.get())
if (pm == null)
pm = pp.match(call.getUrlPath());
return pm;
@@ -1835,8 +1853,8 @@ public class RestOpContext extends Context implements
Comparable<RestOpContext>
if (pm == null)
return 0;
- var rm = getRequiredMatchers();
- var om = getOptionalMatchers();
+ var rm = requiredMatchers.get();
+ var om = optionalMatchers.get();
if (rm.length == 0 && om.length == 0) {
session.urlPathMatch(pm); // Cache so we don't have to
recalculate.
return 2;
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/auth/AuthFilterChain.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/auth/AuthFilterChain.java
index 8a1a0de9f5..179220f08e 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/auth/AuthFilterChain.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/auth/AuthFilterChain.java
@@ -16,6 +16,7 @@
*/
package org.apache.juneau.rest.server.auth;
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
import static org.apache.juneau.commons.utils.CollectionUtils.*;
import static org.apache.juneau.commons.utils.Shorts.*;
import static org.apache.juneau.commons.utils.StringUtils.isEmpty;
@@ -88,8 +89,15 @@ import jakarta.servlet.http.*;
*
* @since 10.0.0
*/
+@SuppressWarnings({
+ "java:S115" // Constants use UPPER_snakeCase convention
+})
public class AuthFilterChain implements Filter, Authenticator {
+ // Argument name constants for assertArgNotNull
+ private static final String ARG_value = "value";
+ private static final String ARG_pattern = "pattern";
+
/**
* A single entry in the chain: an {@link AuthFilter} paired with an
optional path pattern.
*/
@@ -136,6 +144,7 @@ public class AuthFilterChain implements Filter,
Authenticator {
* @return This object.
*/
public Builder append(AuthFilter value) {
+ assertArgNotNull(ARG_value, value);
entries.add(new Entry(value, null));
return this;
}
@@ -152,6 +161,8 @@ public class AuthFilterChain implements Filter,
Authenticator {
* @return This object.
*/
public Builder append(AuthFilter value, String pattern) {
+ assertArgNotNull(ARG_value, value);
+ assertArgNotNullOrBlank(ARG_pattern, pattern);
entries.add(new Entry(value,
UrlPathMatcher.of(pattern)));
return this;
}
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/auth/AuthenticatedRequestWrapper.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/auth/AuthenticatedRequestWrapper.java
index 334e08dfa9..bec91da91f 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/auth/AuthenticatedRequestWrapper.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/auth/AuthenticatedRequestWrapper.java
@@ -16,6 +16,8 @@
*/
package org.apache.juneau.rest.server.auth;
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
import java.security.*;
import java.util.*;
@@ -48,8 +50,15 @@ import jakarta.servlet.http.*;
*
* @since 10.0.0
*/
+@SuppressWarnings({
+ "java:S115" // Constants use UPPER_snakeCase convention
+})
public class AuthenticatedRequestWrapper extends HttpServletRequestWrapper {
+ // Argument name constants for assertArgNotNull
+ private static final String ARG_req = "req";
+ private static final String ARG_result = "result";
+
private final Principal principal;
private final Set<String> roles;
@@ -60,7 +69,8 @@ public class AuthenticatedRequestWrapper extends
HttpServletRequestWrapper {
* @param result The authentication result. Must not be <jk>null</jk>.
*/
public AuthenticatedRequestWrapper(HttpServletRequest req, AuthResult
result) {
- super(req);
+ super(assertArgNotNull(ARG_req, req));
+ assertArgNotNull(ARG_result, result);
this.principal = result.getPrincipal();
this.roles = new HashSet<>(result.getRoles());
}
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/matcher/RestMatcherList.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/matcher/RestMatcherList.java
index 2187d2a5b0..88567e9eb0 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/matcher/RestMatcherList.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/matcher/RestMatcherList.java
@@ -124,12 +124,12 @@ public class RestMatcherList {
*
* @return An unmodifiable list of entries in this list that are
specified as optional.
*/
- public RestMatcher[] getOptionalEntries() { return optionalEntries; }
+ public RestMatcher[] getOptionalEntries() { return
copyOf(optionalEntries); }
/**
* Returns the entries in this list that are specified as required.
*
* @return An unmodifiable list of entries in this list that are
specified as required.
*/
- public RestMatcher[] getRequiredEntries() { return requiredEntries; }
+ public RestMatcher[] getRequiredEntries() { return
copyOf(requiredEntries); }
}
\ No newline at end of file
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/CachingHttpServletRequest.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/CachingHttpServletRequest.java
index 0c766e8de7..b088eac267 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/CachingHttpServletRequest.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/CachingHttpServletRequest.java
@@ -61,9 +61,9 @@ public class CachingHttpServletRequest extends
HttpServletRequestWrapper {
/**
* Returns the content of the servlet request without consuming the
stream.
*
- * @return The content of the request.
+ * @return The content of the request. This is a defensive copy;
modifying it does not affect the cached request body.
*/
- public byte[] getContent() { return content; }
+ public byte[] getContent() { return content.clone(); }
@Override
public ServletInputStream getInputStream() { return new
BoundedServletInputStream(content); }
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/UrlPath.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/UrlPath.java
index 3c12a05443..6957904189 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/UrlPath.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/UrlPath.java
@@ -86,9 +86,9 @@ public class UrlPath {
/**
* Returns the path parts.
*
- * @return The path parts.
+ * @return The path parts. This is a defensive copy; modifying it does
not affect this object's routing state.
*/
- public String[] getParts() { return parts; }
+ public String[] getParts() { return parts.clone(); }
/**
* Returns the raw path passed into this object.