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

jamesbognar pushed a commit to branch juneau-9.2.1-branch
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/juneau-9.2.1-branch by this 
push:
     new 81677c11fb Security fixes for the 9.2.1 release.
81677c11fb is described below

commit 81677c11fb4b35157ea176a97af0ace82126d25e
Author: James Bognar <[email protected]>
AuthorDate: Mon Aug 10 17:30:21 2026 -0400

    Security fixes for the 9.2.1 release.
---
 .../apache/juneau/msgpack/MsgPackInputStream.java  |   5 +-
 .../apache/juneau/parser/ParserInputStream.java    |  44 ++++++++
 .../org/apache/juneau/parser/ParserSession.java    |   6 +
 .../java/org/apache/juneau/swaps/ClassSwap.java    |   7 +-
 .../main/java/org/apache/juneau/xml/XmlReader.java |  11 ++
 .../rest/client/RedirectCredentialGuard.java       |  68 ++++++++++++
 .../juneau/rest/client/RedirectSecurity.java       | 123 +++++++++++++++++++++
 .../org/apache/juneau/rest/client/RestClient.java  |   3 +
 .../org/apache/juneau/http/RedactedHeaders.java    |  96 ++++++++++++++++
 .../java/org/apache/juneau/rest/RestContext.java   |  83 +++++++++++++-
 .../java/org/apache/juneau/rest/RestRequest.java   |  18 ++-
 .../java/org/apache/juneau/rest/RestResponse.java  |  76 ++++++++++++-
 .../java/org/apache/juneau/rest/RestSession.java   |   9 ++
 .../juneau/rest/httppart/RequestContent.java       |  23 +++-
 .../juneau/rest/httppart/RequestFormParams.java    |  47 ++++++++
 .../org/apache/juneau/rest/logger/CallLogger.java  |  44 +++++++-
 .../apache/juneau/http/RedactedHeaders_Test.java   |  55 +++++++++
 .../msgpack/MsgPackParser_MaxLength_Test.java      |  59 ++++++++++
 .../juneau/rest/RestContext_ErrorPosture_Test.java |  64 +++++++++++
 .../juneau/rest/RestRequest_ContentParam_Test.java |  46 ++++++++
 .../rest/RestResponse_SafeRedirect_Test.java       |  54 +++++++++
 .../rest/Rest_PredefinedStatusCodes_Test.java      |  14 +--
 .../juneau/rest/client/RedirectSecurity_Test.java  |  68 ++++++++++++
 .../httppart/RequestContent_BufferSize_Test.java   |  51 +++++++++
 .../httppart/RequestFormParams_MaxInput_Test.java  |  80 ++++++++++++++
 .../juneau/swaps/ClassSwap_NoInitialize_Test.java  |  50 +++++++++
 .../apache/juneau/xml/XmlValidatingDtd_Test.java   |  65 +++++++++++
 27 files changed, 1248 insertions(+), 21 deletions(-)

diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackInputStream.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackInputStream.java
index 60d9d2b24b..6f6a2d528b 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackInputStream.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackInputStream.java
@@ -115,7 +115,10 @@ public class MsgPackInputStream extends ParserInputStream {
         * Read a binary field from the stream.
         */
        byte[] readBinary() throws IOException {
-               var b = new byte[(int)length];
+               // checkLength bounds the declared length against the 
configured maximum (always <= Integer.MAX_VALUE),
+               // so a 32-bit MessagePack length in [2^31, 2^32-1] is rejected 
here rather than truncating to a
+               // negative int and allocating the wrong size.
+               var b = new byte[checkLength(length, "binary")];
                read(b);
                return b;
        }
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserInputStream.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserInputStream.java
index f6ce540a59..e2bab20871 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserInputStream.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserInputStream.java
@@ -16,6 +16,8 @@
  */
 package org.apache.juneau.parser;
 
+import static org.apache.juneau.commons.utils.ThrowableUtils.*;
+
 import java.io.*;
 
 /**
@@ -36,6 +38,8 @@ public class ParserInputStream extends InputStream implements 
Positionable {
 
        private final InputStream is;
        int pos = 0;
+       // Default cap (16 MiB) for wire-declared lengths; overridable via 
setMaxLength.
+       private int maxLength = 16 * 1024 * 1024;
 
        /**
         * Constructor.
@@ -48,6 +52,46 @@ public class ParserInputStream extends InputStream 
implements Positionable {
                pipe.setPositionable(this);
        }
 
+       /**
+        * Sets the maximum length allowed for a single wire-declared 
length/count.
+        *
+        * @param value The maximum length in bytes.  Values &le; 0 disable the 
cap (only the negative-length check remains).
+        */
+       public void setMaxLength(int value) {
+               maxLength = value <= 0 ? Integer.MAX_VALUE : value;
+       }
+
+       /**
+        * Bounds a wire-declared length/count against the configured maximum 
before it is used to size an allocation.
+        *
+        * @param len The declared length/count read off the wire.
+        * @param what A short description of the field being read (for the 
error message).
+        * @return The length as an <c>int</c>, guaranteed to be non-negative 
and within the configured maximum.
+        * @throws IOException If the length is negative (or beyond int range) 
or exceeds the configured maximum.
+        */
+       public int checkLength(long len, String what) throws IOException {
+               return checkLength(len, maxLength, what);
+       }
+
+       /**
+        * Bounds a wire-declared length/count against the specified maximum 
before it is used to size an allocation.
+        *
+        * @param len The declared length/count read off the wire.
+        * @param maxLength The maximum allowed length.  Values &le; 0 are 
treated as {@link Integer#MAX_VALUE}
+        *      (only the negative-length check applies).
+        * @param what A short description of the field being read (for the 
error message).
+        * @return The length as an <c>int</c>, guaranteed to be non-negative 
and within the specified maximum.
+        * @throws IOException If the length is negative (or beyond int range) 
or exceeds the configured maximum.
+        */
+       public static int checkLength(long len, long maxLength, String what) 
throws IOException {
+               var max = maxLength <= 0 ? Integer.MAX_VALUE : maxLength;
+               if (len < 0)
+                       throw ioex("Invalid {0} length (negative): {1}", what, 
len);
+               if (len > max)
+                       throw ioex("{0} length {1} exceeds maximum allowed 
{2}", what, len, max);
+               return (int)len;
+       }
+
        @Override /* Overridden from Positionable */
        public Position getPosition() { return new Position(pos); }
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserSession.java
index 6033b1aa6b..90155083dd 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserSession.java
@@ -620,6 +620,8 @@ public class ParserSession extends BeanSession {
                        throw e;
                } catch (@SuppressWarnings("unused") StackOverflowError e) {
                        throw new ParseException(this, "Depth too deep.  Stack 
overflow occurred.");
+               } catch (@SuppressWarnings("unused") OutOfMemoryError e) {
+                       throw new ParseException(this, "Out of memory occurred. 
 Input too large to parse.");
                } catch (IOException e) {
                        throw new ParseException(this, e, "I/O exception 
occurred.  exception={0}, message={1}.", cns(e), lm(e));
                } catch (Exception e) {
@@ -655,6 +657,8 @@ public class ParserSession extends BeanSession {
                        throw e;
                } catch (@SuppressWarnings("unused") StackOverflowError e) {
                        throw new ParseException(this, "Depth too deep.  Stack 
overflow occurred.");
+               } catch (@SuppressWarnings("unused") OutOfMemoryError e) {
+                       throw new ParseException(this, "Out of memory occurred. 
 Input too large to parse.");
                } catch (IOException e) {
                        throw new ParseException(this, e, "I/O exception 
occurred.  exception={0}, message={1}.", cns(e), lm(e));
                } catch (Exception e) {
@@ -723,6 +727,8 @@ public class ParserSession extends BeanSession {
                        throw e;
                } catch (@SuppressWarnings("unused") StackOverflowError e) {
                        throw new ParseException(this, "Depth too deep.  Stack 
overflow occurred.");
+               } catch (@SuppressWarnings("unused") OutOfMemoryError e) {
+                       throw new ParseException(this, "Out of memory occurred. 
 Input too large to parse.");
                } catch (Exception e) {
                        throw new ParseException(this, e, "Exception occurred.  
exception={0}, message={1}.", cns(e), lm(e));
                } finally {
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/ClassSwap.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/ClassSwap.java
index 316df905bf..57aa143433 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/ClassSwap.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/swaps/ClassSwap.java
@@ -35,6 +35,11 @@ public class ClassSwap extends StringSwap<Class<?>> {
 
        @Override /* Overridden from ObjectSwap */
        public Class<?> unswap(BeanSession session, String o, ClassMeta<?> 
hint) throws Exception {
-               return o == null ? null : Class.forName(o);
+               if (o == null)
+                       return null;
+               // Resolve without initializing — a parsed class name must 
never trigger the named class's static
+               // initializer as a side effect.  The class loader matches 
Class.forName(String)'s implicit caller
+               // loader, so resolvable names continue to resolve exactly as 
before.
+               return Class.forName(o, false, 
ClassSwap.class.getClassLoader());
        }
 }
\ No newline at end of file
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlReader.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlReader.java
index 18808d555a..ce1faf25c2 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlReader.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlReader.java
@@ -21,6 +21,7 @@ import static org.apache.juneau.commons.utils.Utils.*;
 
 import java.io.*;
 
+import javax.xml.*;
 import javax.xml.namespace.*;
 import javax.xml.stream.*;
 import javax.xml.stream.util.*;
@@ -64,6 +65,16 @@ public class XmlReader implements XMLStreamReader, 
Positionable {
                        factory.setProperty(XMLInputFactory.IS_COALESCING, 
true);
                        
factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
                        
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
+                       // Disable DTD processing unconditionally.  DTD 
constructs are not needed for data binding, and
+                       // keeping them off — regardless of the validating flag 
— bounds document/entity handling so a
+                       // declared internal DTD cannot drive runaway entity 
expansion.
+                       factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
+                       // Belt-and-suspenders: forbid resolving any external 
DTD/schema on factories that honor these
+                       // properties, in case a StAX implementation handles 
external-entity support inconsistently.
+                       if 
(factory.isPropertySupported(XMLConstants.ACCESS_EXTERNAL_DTD))
+                               
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+                       if 
(factory.isPropertySupported(XMLConstants.ACCESS_EXTERNAL_SCHEMA))
+                               
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
                        if 
(factory.isPropertySupported(XMLInputFactory.REPORTER) && nn(reporter))
                                factory.setProperty(XMLInputFactory.REPORTER, 
reporter);
                        if 
(factory.isPropertySupported(XMLInputFactory.RESOLVER) && nn(resolver))
diff --git 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RedirectCredentialGuard.java
 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RedirectCredentialGuard.java
new file mode 100644
index 0000000000..b3ea250f2c
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RedirectCredentialGuard.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.client;
+
+import java.net.*;
+
+import org.apache.http.*;
+import org.apache.http.protocol.*;
+
+/**
+ * Request interceptor that removes caller-set credential headers before a 
request is replayed against a
+ * different origin as the result of a {@code 3xx} redirect.
+ *
+ * <p>
+ * The redirect executor copies the original request's headers &mdash; 
including {@code Authorization} and
+ * {@code Cookie} &mdash; onto the follow-up request aimed at the {@code 
Location} target.  When that target is
+ * a different scheme, host, or port (or an {@code https}&rarr;{@code http} 
downgrade), forwarding those
+ * headers would disclose credentials to an unrelated origin.  This 
interceptor runs on every outgoing request
+ * in an exchange: it records the origin of the first request and, on any 
later request in the same exchange
+ * whose origin differs, strips the credential headers before the request 
leaves the client.
+ *
+ * <p>
+ * The forward/strip decision and the header set are delegated to {@link 
RedirectSecurity}.
+ */
+final class RedirectCredentialGuard implements HttpRequestInterceptor {
+
+       private static final String ATTR_ORIGIN = 
RedirectCredentialGuard.class.getName() + ".origin";
+
+       @Override /* HttpRequestInterceptor */
+       public void process(HttpRequest request, HttpContext context) {
+               var target = HttpCoreContext.adapt(context).getTargetHost();
+               if (target == null)
+                       return;
+               var current = originOf(target);
+               if (current == null)
+                       return;
+               var origin = (URI)context.getAttribute(ATTR_ORIGIN);
+               if (origin == null) {
+                       context.setAttribute(ATTR_ORIGIN, current);
+                       return;
+               }
+               if (RedirectSecurity.shouldStripCredentials(origin, current))
+                       for (var name : RedirectSecurity.stripOnCrossOrigin())
+                               request.removeHeaders(name);
+       }
+
+       private static URI originOf(HttpHost host) {
+               try {
+                       return URI.create(host.toURI());
+               } catch (IllegalArgumentException e) {
+                       return null;
+               }
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RedirectSecurity.java
 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RedirectSecurity.java
new file mode 100644
index 0000000000..f3bc0c736b
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RedirectSecurity.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.client;
+
+import java.net.*;
+import java.util.*;
+
+import org.apache.juneau.http.*;
+
+/**
+ * Pure decision logic for whether caller-set credential headers (such as 
{@code Authorization}
+ * or {@code Cookie}) should be forwarded or stripped when a REST call is 
transparently replayed
+ * against a redirect target (an HTTP {@code 3xx} response's {@code Location}).
+ *
+ * <p>
+ * This class contains only the <b>decision</b> &mdash; it does not itself 
intercept, follow, or
+ * rewrite any request.  The transport binding is responsible for calling into 
this class from its
+ * own redirect hook and acting on the result.
+ *
+ * <p>
+ * The policy is intentionally simple and conservative:
+ * <ul>
+ *     <li>Credentials are forwarded only when the redirect target is the 
<b>exact same origin</b>
+ *             as the original request &mdash; same scheme, same host 
(case-insensitive), and same port
+ *             (after applying the standard default port for {@code 
http}/{@code https}).
+ *     <li>Credentials are stripped on any other origin change (different 
scheme, host, or port).
+ *     <li>Credentials are stripped on an {@code https} &rarr; {@code http} 
scheme downgrade.
+ * </ul>
+ *
+ * <p>
+ * Both {@code from} and {@code to} must be absolute URIs (non-{@code null} 
scheme and host);
+ * callers are responsible for resolving a relative {@code Location} header 
against the original
+ * request URI before calling into this class.
+ */
+public final class RedirectSecurity {
+
+       private RedirectSecurity() {}
+
+       /**
+        * Returns {@code true} if {@code from} and {@code to} share the same 
origin &mdash; scheme,
+        * host (case-insensitive), and port (after default-port normalization 
for {@code http}/{@code https}).
+        *
+        * @param from The original request URI.  Must be absolute (non-{@code 
null} scheme and host).
+        * @param to The redirect target URI.  Must be absolute (non-{@code 
null} scheme and host).
+        * @return {@code true} if both URIs resolve to the same origin.
+        * @throws IllegalArgumentException If either argument is {@code null} 
or not an absolute URI.
+        */
+       public static boolean sameOrigin(URI from, URI to) {
+               requireAbsolute(from, "from");
+               requireAbsolute(to, "to");
+               return from.getScheme().equalsIgnoreCase(to.getScheme())
+                       && from.getHost().equalsIgnoreCase(to.getHost())
+                       && normalizedPort(from) == normalizedPort(to);
+       }
+
+       /**
+        * Returns {@code true} if the redirect represents an {@code https} 
&rarr; {@code http} scheme downgrade.
+        *
+        * @param from The original request URI.  Must be absolute (non-{@code 
null} scheme and host).
+        * @param to The redirect target URI.  Must be absolute (non-{@code 
null} scheme and host).
+        * @return {@code true} if {@code from} is {@code https} and {@code to} 
is {@code http}.
+        * @throws IllegalArgumentException If either argument is {@code null} 
or not an absolute URI.
+        */
+       public static boolean isDowngrade(URI from, URI to) {
+               requireAbsolute(from, "from");
+               requireAbsolute(to, "to");
+               return "https".equalsIgnoreCase(from.getScheme()) && 
"http".equalsIgnoreCase(to.getScheme());
+       }
+
+       /**
+        * Convenience boolean form of the forward/strip decision for a single 
redirect hop.
+        *
+        * @param from The original request URI.  Must be absolute (non-{@code 
null} scheme and host).
+        * @param to The redirect target URI.  Must be absolute (non-{@code 
null} scheme and host).
+        * @return {@code true} if credentials must be stripped before 
replaying the request against {@code to}.
+        * @throws IllegalArgumentException If either argument is {@code null} 
or not an absolute URI.
+        */
+       public static boolean shouldStripCredentials(URI from, URI to) {
+               return ! sameOrigin(from, to) || isDowngrade(from, to);
+       }
+
+       /**
+        * Returns the header names that must be stripped from a request before 
it is replayed cross-origin.
+        * Reuses the canonical credential-header set from {@link 
RedactedHeaders#DEFAULT}.
+        *
+        * @return The set of header names to strip on a cross-origin (or 
downgraded) redirect.
+        */
+       public static Set<String> stripOnCrossOrigin() {
+               return RedactedHeaders.DEFAULT;
+       }
+
+       private static int normalizedPort(URI uri) {
+               var port = uri.getPort();
+               if (port != -1)
+                       return port;
+               return switch (uri.getScheme().toLowerCase(Locale.ROOT)) {
+                       case "https" -> 443;
+                       case "http" -> 80;
+                       default -> -1;
+               };
+       }
+
+       private static void requireAbsolute(URI uri, String argName) {
+               if (uri == null)
+                       throw new IllegalArgumentException("Argument '" + 
argName + "' must not be null");
+               if (uri.getScheme() == null || uri.getHost() == null)
+                       throw new IllegalArgumentException("Argument '" + 
argName + "' must be an absolute URI with a scheme and host: " + uri);
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
index 065d3837a8..cb353d05fa 100644
--- 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
+++ 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
@@ -6034,6 +6034,9 @@ public class RestClient extends BeanContextable 
implements HttpClient, Closeable
                        if (connectionManager == null)
                                connectionManager = createConnectionManager();
                        
httpClientBuilder().setConnectionManager(connectionManager);
+                       // Prevent the default redirect executor from replaying 
caller-set credential headers (Authorization,
+                       // Cookie, etc.) to a redirect target on a different 
origin or a downgraded scheme.
+                       httpClientBuilder().addInterceptorLast(new 
RedirectCredentialGuard());
                        return httpClientBuilder().build();
                }
 
diff --git 
a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/RedactedHeaders.java
 
b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/RedactedHeaders.java
new file mode 100644
index 0000000000..059e53204b
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/RedactedHeaders.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.http;
+
+import java.util.*;
+
+/**
+ * Utility for masking the values of header names that commonly carry account 
credentials
+ * (bearer tokens, session cookies, API keys, and the like) before they are 
written somewhere
+ * observable, such as a log line or an echoed-back response body.
+ *
+ * <p>
+ * Matching is always case-insensitive, mirroring HTTP header-name semantics.
+ *
+ * <p>
+ * Callers that need a different or extended header set (for example, an 
additional
+ * internal-trace header) can pass their own name collection to the 
two-argument overloads
+ * rather than being limited to {@link #DEFAULT}.
+ */
+public final class RedactedHeaders {
+
+       /** Sentinel value substituted for a masked header value. */
+       public static final String REDACTED = "[REDACTED]";
+
+       /** Canonical set of header names commonly used to carry credentials, 
matched case-insensitively. */
+       public static final Set<String> DEFAULT = Set.of(
+               "Authorization", "Cookie", "Set-Cookie", "Proxy-Authorization", 
"X-API-Key");
+
+       private RedactedHeaders() {}
+
+       /**
+        * Returns {@code true} if the given header name is in {@link #DEFAULT} 
(case-insensitive).
+        *
+        * @param name The header name to test. Can be {@code null} (returns 
{@code false}).
+        * @return {@code true} if the name matches one of {@link #DEFAULT}, 
case-insensitively.
+        */
+       public static boolean isSensitive(String name) {
+               return isSensitive(name, DEFAULT);
+       }
+
+       /**
+        * Returns {@code true} if the given header name matches one of {@code 
names}, case-insensitively.
+        *
+        * @param name The header name to test. Can be {@code null} (returns 
{@code false}).
+        * @param names The candidate header names. Can be {@code null} or 
empty (returns {@code false}).
+        *      {@code null} elements are ignored.
+        * @return {@code true} if {@code name} matches one of {@code names}, 
case-insensitively.
+        */
+       public static boolean isSensitive(String name, Collection<String> 
names) {
+               if (name == null || names == null)
+                       return false;
+               for (var n : names)
+                       if (n != null && name.equalsIgnoreCase(n))
+                               return true;
+               return false;
+       }
+
+       /**
+        * Masks {@code value} with {@link #REDACTED} if {@code name} is in 
{@link #DEFAULT}
+        * (case-insensitive); otherwise returns {@code value} unchanged.
+        *
+        * @param name The header name. Can be {@code null} (never matches; 
{@code value} is returned as-is).
+        * @param value The header value to mask. Can be {@code null}.
+        * @return {@link #REDACTED} if {@code name} is sensitive, else {@code 
value}.
+        */
+       public static String redact(String name, String value) {
+               return isSensitive(name) ? REDACTED : value;
+       }
+
+       /**
+        * Masks {@code value} with {@link #REDACTED} if {@code name} matches 
one of {@code names}
+        * (case-insensitive); otherwise returns {@code value} unchanged.
+        *
+        * @param name The header name. Can be {@code null} (never matches; 
{@code value} is returned as-is).
+        * @param value The header value to mask. Can be {@code null}.
+        * @param names The candidate header names. Can be {@code null} or 
empty (nothing is masked).
+        * @return {@link #REDACTED} if {@code name} matches one of {@code 
names}, else {@code value}.
+        */
+       public static String redact(String name, String value, 
Collection<String> names) {
+               return isSensitive(name, names) ? REDACTED : value;
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index 436b9dade7..c045d78b81 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -251,7 +251,7 @@ public class RestContext extends Context {
                private List<MediaType> produces;
                private List<Object> children = list();
                private Logger logger;
-               private long maxInput = 
env("RestContext.maxInput").map(StringUtils::parseLongWithSuffix).orElse(100_000_000l);
+               private long maxInput = 
env("RestContext.maxInput").map(StringUtils::parseLongWithSuffix).orElse(1_000_000l);
                private MethodExecStore.Builder methodExecStore;
                private MethodList destroyMethods;
                private MethodList endCallMethods;
@@ -6010,16 +6010,16 @@ public class RestContext extends Context {
                        if (r.value().length > 0)
                                code = r.value()[0];
 
+               var appThrown = e instanceof BasicHttpException;
                var e2 = (e instanceof BasicHttpException e3 ? e3 : new 
BasicHttpException(code, e));
 
                var req = session.getRequest();
                var res = session.getResponse();
 
-               Throwable t = e2.getRootCause();
-               if (nn(t)) {
-                       Thrown t2 = thrown(t);
+               var rpcDispatch = isRpcDispatch(session);
+               var t2 = resolveThrownHeader(e2, isRenderResponseStackTraces(), 
rpcDispatch);
+               if (nn(t2))
                        res.setHeader(t2.getName(), t2.getValue());
-               }
 
                try {
                        res.setContentType("text/plain");
@@ -6040,8 +6040,13 @@ public class RestContext extends Context {
                                        w2.append("HTTP 
").append(String.valueOf(statusCode)).append(": 
").append(httpMessage).append("\n\n");
                                if (isRenderResponseStackTraces())
                                        e.printStackTrace(w2);
-                               else
+                               else if (rpcDispatch)
+                                       // RRPC is an internal transport whose 
client proxy relies on the full detail to reconstruct
+                                       // the server-side exception; it is not 
a general-purpose response surface, so it keeps the
+                                       // detailed body rather than the 
suppressed message used for ordinary responses.
                                        w2.append(e2.getFullStackMessage(true));
+                               else
+                                       
w2.append(suppressedErrorBodyMessage(e2, appThrown));
                        }
 
                } catch (Exception e1) {
@@ -6049,6 +6054,72 @@ public class RestContext extends Context {
                }
        }
 
+       /**
+        * Determines whether a {@code Thrown} header carrying root-cause 
detail should be attached to an error response.
+        *
+        * <p>
+        * The header echoes internal exception detail, so it is only emitted 
when stack-trace rendering is explicitly
+        * enabled, or for RRPC dispatch targets where the client proxy relies 
on it to reconstruct the thrown exception.
+        *
+        * <p>
+        * Package-private so it can be unit-tested directly.
+        *
+        * @param e2 The (possibly wrapped) exception being reported.  Must not 
be <jk>null</jk>.
+        * @param renderResponseStackTraces Whether the resource is configured 
to render stack traces in responses.
+        * @param rpcDispatch Whether the matched operation is an RRPC dispatch 
target.
+        * @return The header to set, or <jk>null</jk> if none should be set.
+        */
+       static Thrown resolveThrownHeader(BasicHttpException e2, boolean 
renderResponseStackTraces, boolean rpcDispatch) {
+               if (!renderResponseStackTraces && !rpcDispatch)
+                       return null;
+               var t = e2.getRootCause();
+               return nn(t) ? thrown(t) : null;
+       }
+
+       /**
+        * Returns <jk>true</jk> if the operation matched for the given session 
is an RRPC dispatch target.
+        *
+        * <p>
+        * Used by {@link #handleError(RestSession, Throwable)} to decide 
whether the {@code Thrown} header should be
+        * included regardless of {@link #isRenderResponseStackTraces()}.
+        *
+        * @param session The rest call.  Must not be <jk>null</jk>.
+        * @return <jk>true</jk> if the matched operation (if any) is an RRPC 
dispatch target.
+        */
+       static boolean isRpcDispatch(RestSession session) {
+               var opSession = session.getOpSessionOrNull();
+               return nn(opSession) && opSession.getContext() instanceof 
RrpcRestOpContext;
+       }
+
+       /**
+        * Determines the message written to the plain-text error response body 
when stack-trace rendering is off.
+        *
+        * <p>
+        * Only application-authored exceptions (those thrown as a {@link 
BasicHttpException} with a client-facing
+        * message in mind) have their message echoed; anything else is treated 
as internal detail and suppressed.
+        *
+        * <p>
+        * Package-private so it can be unit-tested directly.
+        *
+        * @param e2 The (possibly wrapped) exception being reported.  Must not 
be <jk>null</jk>.
+        * @param appThrown <jk>true</jk> if the original, unwrapped exception 
was itself a {@link BasicHttpException}.
+        * @return The scrubbed message to write, or an empty string if nothing 
should be written.
+        */
+       static String suppressedErrorBodyMessage(BasicHttpException e2, boolean 
appThrown) {
+               return appThrown ? scrubForXss(e2.getMessage()) : "";
+       }
+
+       /**
+        * Replaces {@code <}, {@code >}, and {@code &} in a message about to 
be written into a {@code text/plain} error
+        * response body, as a simple defense against the body being reflected 
into an HTML context downstream.
+        *
+        * @param msg The message to scrub.  Can be <jk>null</jk>.
+        * @return The scrubbed message, or an empty string if {@code msg} was 
<jk>null</jk>.
+        */
+       private static String scrubForXss(String msg) {
+               return msg == null ? "" : msg.replace('<', ' ').replace('>', ' 
').replace('&', ' ');
+       }
+
        /**
         * Handle the case where a matching method was not found.
         *
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
index c81104fc11..013f435d16 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
@@ -208,6 +208,22 @@ public class RestRequest extends HttpServletRequestWrapper 
{
 
        private Charset charset;
 
+       /**
+        * Returns <jk>true</jk> if <c>method</c> is one of the methods the 
<c>content</c> URL-parameter body
+        * override (see {@link 
org.apache.juneau.rest.annotation.Rest#disableContentParam()}) is documented to 
apply to.
+        *
+        * <p>
+        * Restricting the override to <c>PUT</c>/<c>POST</c> prevents a plain 
cross-origin <c>GET</c> (which triggers
+        * no preflight and carries no attacker-controlled header) from 
smuggling an attacker-chosen request body into
+        * an endpoint that would otherwise require a body-carrying method.
+        *
+        * @param method The request's HTTP method. Can be <jk>null</jk> 
(returns <jk>false</jk>).
+        * @return <jk>true</jk> if <c>method</c> is <c>PUT</c> or <c>POST</c>, 
case-insensitively.
+        */
+       static boolean isContentParamMethod(String method) {
+               return "PUT".equalsIgnoreCase(method) || 
"POST".equalsIgnoreCase(method);
+       }
+
        /**
         * Constructor.
         */
@@ -227,7 +243,7 @@ public class RestRequest extends HttpServletRequestWrapper {
 
                content = new RequestContent(this);
 
-               if (context.isAllowContentParam()) {
+               if (context.isAllowContentParam() && 
isContentParamMethod(inner.getMethod())) {
                        var b = 
queryParams.get("content").asString().orElse(null);
                        if (nn(b)) {
                                headers.set("Content-Type", 
UonSerializer.DEFAULT.getResponseContentType());
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
index 98e59f85fe..3c27a53f3f 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
@@ -122,7 +122,7 @@ public class RestResponse extends 
HttpServletResponseWrapper {
        private Optional<HttpPartSchema> contentSchema;
        private Serializer serializer;
        private Optional<SerializerMatch> serializerMatch;
-       private boolean safeHeaders;
+       private boolean safeHeaders = true;
        private int maxHeaderLength = 8096;
 
        /**
@@ -530,6 +530,12 @@ public class RestResponse extends 
HttpServletResponseWrapper {
         * Relative URIs are always interpreted as relative to the context root.
         * This is similar to how WAS handles redirect requests, and is 
different from how Tomcat handles redirect requests.
         *
+        * <p>
+        * This method forwards <c>uri</c> to the container as-is.  If the 
target is built from request input (e.g.
+        * a query parameter or header value), a caller can steer users to an 
arbitrary external site; use
+        * {@link #sendSafeRedirect(String,String...) 
sendSafeRedirect(String,String...)} instead in that case, which
+        * validates the target is either relative or points at an allow-listed 
host.
+        *
         * @param uri The redirection URL.
         * @throws IOException If an input or output exception occurs
         */
@@ -541,6 +547,55 @@ public class RestResponse extends 
HttpServletResponseWrapper {
                inner.sendRedirect(uri);
        }
 
+       /**
+        * Redirects to the specified URI, but only if it's a relative path 
(interpreted per {@link #sendRedirect(String)
+        * sendRedirect(String)}) or an absolute/protocol-relative URI whose 
host matches one of <c>allowedHosts</c>.
+        *
+        * <p>
+        * Intended for use when the redirect target is derived from request 
input rather than a fixed, trusted
+        * literal, so that input can't be used to redirect a user to an 
arbitrary external site.
+        *
+        * @param uri The redirection URL.
+        * @param allowedHosts The hosts (case-insensitive) an absolute or 
protocol-relative <c>uri</c> is allowed to target.  Ignored for relative URIs.
+        * @throws IOException If an input or output exception occurs.
+        * @throws BadRequest If <c>uri</c> is absolute or protocol-relative 
and its host isn't in <c>allowedHosts</c>.
+        */
+       public void sendSafeRedirect(String uri, String...allowedHosts) throws 
IOException {
+               if (! isSafeRedirectUri(uri, allowedHosts))
+                       throw new BadRequest("Redirect target ''{0}'' is not a 
relative path or an allowed host.", uri);
+               sendRedirect(uri);
+       }
+
+       /**
+        * Returns <jk>true</jk> if <c>uri</c> is safe to pass to {@link 
#sendRedirect(String) sendRedirect(String)}
+        * without risk of redirecting to an unintended external site: either a 
relative path, or an absolute/
+        * protocol-relative URI whose host is in <c>allowedHosts</c>.
+        *
+        * @param uri The candidate redirection URL.
+        * @param allowedHosts The hosts (case-insensitive) an absolute or 
protocol-relative <c>uri</c> is allowed to target.
+        * @return <jk>true</jk> if the URI is safe to redirect to.
+        */
+       static boolean isSafeRedirectUri(String uri, String...allowedHosts) {
+               if (uri == null || uri.isEmpty())
+                       return true;
+               if (uri.charAt(0) == '/' && ! uri.startsWith("//"))
+                       return true;
+               if (uri.indexOf("://") == -1 && ! uri.startsWith("//"))
+                       return true;
+               String host;
+               try {
+                       host = new java.net.URI(uri.startsWith("//") ? "http:" 
+ uri : uri).getHost();
+               } catch (java.net.URISyntaxException e) {
+                       return false;
+               }
+               if (host == null || allowedHosts == null)
+                       return false;
+               for (var h : allowedHosts)
+                       if (host.equalsIgnoreCase(h))
+                               return true;
+               return false;
+       }
+
        /**
         * Shortcut for calling <c>getRequest().setAttribute(String,Object)</c>.
         *
@@ -798,6 +853,10 @@ public class RestResponse extends 
HttpServletResponseWrapper {
         * When enabled, invalid characters such as CTRL characters will be 
stripped from header values
         * before they get set.
         *
+        * <p>
+        * This is now the default behavior; this method is retained so callers 
that enabled it explicitly continue
+        * to compile.  Use {@link #setUnsafeHeaders()} to opt back out in a 
fully-trusted setting.
+        *
         * @return This object.
         */
        public RestResponse setSafeHeaders() {
@@ -805,6 +864,21 @@ public class RestResponse extends 
HttpServletResponseWrapper {
                return this;
        }
 
+       /**
+        * Disables safe-header mode.
+        *
+        * <p>
+        * Safe-header mode (stripping invalid characters such as CTRL 
characters from header values before they are
+        * set) is enabled by default.  This method opts back out, restoring 
verbatim header pass-through for the rare
+        * case where a caller needs it in a fully-trusted setting.
+        *
+        * @return This object.
+        */
+       public RestResponse setUnsafeHeaders() {
+               this.safeHeaders = false;
+               return this;
+       }
+
        private Object getRawOutput() { return content == null ? null : 
content.orElse(null); }
 
        private FinishablePrintWriter getWriter(boolean raw, boolean autoflush) 
throws NotAcceptable, IOException {
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestSession.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestSession.java
index f17ca79836..a0491fdc26 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestSession.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestSession.java
@@ -362,6 +362,15 @@ public class RestSession extends ContextSession {
                return opSession;
        }
 
+       /**
+        * Returns the operation session for this request, or <jk>null</jk> if 
one has not been created yet.
+        *
+        * @return The operation session, or <jk>null</jk> if not yet created.
+        */
+       public RestOpSession getOpSessionOrNull() {
+               return opSession;
+       }
+
        /**
         * Shortcut for calling <c>getRequest().getPathInfo()</c>.
         *
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/httppart/RequestContent.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/httppart/RequestContent.java
index 193b65bede..5406d28d27 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/httppart/RequestContent.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/httppart/RequestContent.java
@@ -409,8 +409,27 @@ public class RequestContent {
                if (r instanceof BufferedReader r2)
                        return r2;
                int len = req.getHttpServletRequest().getContentLength();
-               int buffSize = len <= 0 ? 8192 : Math.max(len, 8192);
-               return new BufferedReader(r, buffSize);
+               return new BufferedReader(r, computeReaderBufferSize(len, 
maxInput));
+       }
+
+       /**
+        * Computes the initial char-buffer size for {@link #getReader()}.
+        *
+        * <p>
+        * The buffer size is derived from the client-supplied content length, 
so it is clamped to the effective
+        * maximum-input ceiling to keep a spoofed length from driving an 
oversized pre-allocation regardless of the
+        * actual body size.  Package-private so the sizing decision can be 
exercised directly in unit tests.
+        *
+        * @param declaredContentLength The request's declared content length.  
May be {@code <= 0} if absent/unparsable.
+        * @param maxInput The effective maximum-input ceiling in bytes.  
Values {@code <= 0} are treated as
+        *      "no additional ceiling beyond {@link Integer#MAX_VALUE}".
+        * @return The buffer size to use, at least the historical {@code 
8192}-byte default and never past
+        *      {@code min(maxInput, Integer.MAX_VALUE)}.
+        */
+       static int computeReaderBufferSize(int declaredContentLength, long 
maxInput) {
+               long ceiling = maxInput > 0 ? maxInput : Integer.MAX_VALUE;
+               long buffSize = declaredContentLength <= 0 ? 8192 : 
Math.min(Math.max(declaredContentLength, 8192), ceiling);
+               return (int)Math.min(buffSize, Integer.MAX_VALUE);
        }
 
        /**
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/httppart/RequestFormParams.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/httppart/RequestFormParams.java
index c472a17f8c..f85f01f9f7 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/httppart/RequestFormParams.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/httppart/RequestFormParams.java
@@ -34,6 +34,7 @@ import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.http.*;
 import org.apache.juneau.http.header.*;
 import org.apache.juneau.http.part.*;
+import org.apache.juneau.http.response.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.rest.*;
 import org.apache.juneau.rest.util.*;
@@ -143,7 +144,13 @@ public class RequestFormParams extends 
ArrayList<RequestFormParam> {
                if (content.isLoaded() || ! 
req.getHeader(ContentType.class).orElse(ContentType.NULL).equalsIgnoreCase("multipart/form-data"))
                        m = RestUtils.parseQuery(content.getReader());
                else {
+                       // Enforce the effective maxInput ceiling against 
multipart bodies, which otherwise route around the
+                       // bounded input stream used for non-multipart content. 
 Reject early on the declared content length,
+                       // then again on the cumulative parsed-part size as a 
backstop when the length was absent or wrong.
+                       var maxInput = req.getOpContext().getMaxInput();
+                       checkMultipartLength(maxInput, 
req.getHttpServletRequest().getContentLengthLong());
                        c = req.getHttpServletRequest().getParts();
+                       checkMultipartLength(maxInput, c);
                        if (c == null || c.isEmpty())
                                m = 
req.getHttpServletRequest().getParameterMap();
                }
@@ -173,6 +180,46 @@ public class RequestFormParams extends 
ArrayList<RequestFormParam> {
                }
        }
 
+       /**
+        * Rejects a {@code multipart/form-data} request outright when its 
declared {@code Content-Length} already
+        * exceeds {@code maxInput}, before the servlet container is asked to 
parse (and potentially buffer to disk)
+        * the body.
+        *
+        * <p>
+        * Package-private so it can be unit-tested directly.
+        *
+        * @param maxInput The effective maximum input ceiling in bytes.  
Values {@code <= 0} disable the check.
+        * @param declaredContentLength The request's declared content length.  
Values {@code < 0} (absent/unparsable)
+        *      are ignored; the post-parse {@link #checkMultipartLength(long, 
Collection)} check still applies.
+        * @throws PayloadTooLarge If {@code declaredContentLength} exceeds 
{@code maxInput}.
+        */
+       static void checkMultipartLength(long maxInput, long 
declaredContentLength) {
+               if (maxInput > 0 && declaredContentLength > maxInput)
+                       throw new PayloadTooLarge("Multipart request content 
exceeds the maximum allowed input size of {0} bytes.", maxInput);
+       }
+
+       /**
+        * Rejects a {@code multipart/form-data} request once the cumulative 
size of its parsed parts exceeds
+        * {@code maxInput}, as a backstop for the case where the declared 
{@code Content-Length} was absent or wrong.
+        *
+        * <p>
+        * Package-private so it can be unit-tested directly.
+        *
+        * @param maxInput The effective maximum input ceiling in bytes.  
Values {@code <= 0} disable the check.
+        * @param parts The parsed request parts.  Can be {@code null} or empty 
(nothing is checked).
+        * @throws PayloadTooLarge If the cumulative {@link Part#getSize()} 
across {@code parts} exceeds {@code maxInput}.
+        */
+       static void checkMultipartLength(long maxInput, Collection<Part> parts) 
{
+               if (maxInput <= 0 || parts == null || parts.isEmpty())
+                       return;
+               var total = 0L;
+               for (var p : parts) {
+                       total += p.getSize();
+                       if (total > maxInput)
+                               throw new PayloadTooLarge("Multipart request 
content exceeds the maximum allowed input size of {0} bytes.", maxInput);
+               }
+       }
+
        /**
         * Copy constructor.
         */
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/logger/CallLogger.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/logger/CallLogger.java
index 61cb36b3bd..8c53a4d418 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/logger/CallLogger.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/logger/CallLogger.java
@@ -33,6 +33,7 @@ import org.apache.juneau.*;
 import org.apache.juneau.commons.collections.*;
 import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.cp.*;
+import org.apache.juneau.http.RedactedHeaders;
 import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.rest.stats.*;
 import org.apache.juneau.rest.util.*;
@@ -107,6 +108,7 @@ public class CallLogger {
                Predicate<HttpServletRequest> enabledTest;
                CallLoggingDetail requestDetail, responseDetail;
                Level level;
+               Set<String> redactedHeaders = new 
LinkedHashSet<>(RedactedHeaders.DEFAULT);
 
                /**
                 * Constructor.
@@ -402,6 +404,41 @@ public class CallLogger {
                                thrownStore = value;
                        return this;
                }
+
+               /**
+                * Replaces the redacted-header set with the supplied values.
+                *
+                * <p>
+                * Applies to request/response header values written at {@link 
CallLoggingDetail#HEADER HEADER}/
+                * {@link CallLoggingDetail#ENTITY ENTITY} detail, regardless 
of matched rule.  Header names are
+                * matched case-insensitively.  Pass an empty array to disable 
redaction and restore the old
+                * unredacted behavior (not recommended outside of a trusted 
environment or integration tests).
+                *
+                * @param values The header names to redact.  Can be 
<jk>null</jk> (equivalent to an empty array
+                *      &mdash; disables redaction); <jk>null</jk> or blank 
elements are skipped.
+                * @return This object.
+                */
+               public Builder redactedHeaders(String...values) {
+                       redactedHeaders.clear();
+                       if (values != null)
+                               for (var v : values)
+                                       if (v != null && ! v.isBlank())
+                                               redactedHeaders.add(v);
+                       return this;
+               }
+
+               /**
+                * Adds an additional header name to the redacted-header set.
+                *
+                * @param value The header name to redact (case-insensitive).  
Must not be <jk>null</jk> or blank.
+                * @return This object.
+                */
+               public Builder redactHeader(String value) {
+                       if (isBlank(value))
+                               throw new IllegalArgumentException("Argument 
'value' must not be null or blank");
+                       redactedHeaders.add(value);
+                       return this;
+               }
        }
 
        /** Represents no logger */
@@ -497,6 +534,7 @@ public class CallLogger {
        private final Predicate<HttpServletRequest> enabledTest;
        private final Level level;
        private final CallLoggingDetail requestDetail, responseDetail;
+       private final Set<String> redactedHeaders;
 
        /**
         * Constructor.
@@ -516,6 +554,7 @@ public class CallLogger {
                this.requestDetail = builder.requestDetail;
                this.responseDetail = builder.responseDetail;
                this.level = builder.level;
+               this.redactedHeaders = Set.copyOf(builder.redactedHeaders);
        }
 
        /**
@@ -533,6 +572,7 @@ public class CallLogger {
                this.requestDetail = builder.requestDetail;
                this.responseDetail = builder.responseDetail;
                this.level = builder.level;
+               this.redactedHeaders = Set.copyOf(builder.redactedHeaders);
        }
 
        /**
@@ -611,7 +651,7 @@ public class CallLogger {
                                        sb.append("\n---Request Headers---");
                                        while (hh.hasMoreElements()) {
                                                var h = hh.nextElement();
-                                               
sb.append("\n\t").append(h).append(": ").append(req.getHeader(h));
+                                               
sb.append("\n\t").append(h).append(": ").append(RedactedHeaders.redact(h, 
req.getHeader(h), redactedHeaders));
                                        }
                                }
                        }
@@ -621,7 +661,7 @@ public class CallLogger {
                                if (hh.size() > 0) {
                                        sb.append("\n---Response Headers---");
                                        for (var h : hh) {
-                                               
sb.append("\n\t").append(h).append(": ").append(res.getHeader(h));
+                                               
sb.append("\n\t").append(h).append(": ").append(RedactedHeaders.redact(h, 
res.getHeader(h), redactedHeaders));
                                        }
                                }
                        }
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/http/RedactedHeaders_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/http/RedactedHeaders_Test.java
new file mode 100644
index 0000000000..507d0c9d2c
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/http/RedactedHeaders_Test.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.http;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Verifies that header values commonly carrying credentials are masked before 
being written somewhere
+ * observable, while non-sensitive headers pass through unchanged.
+ */
+class RedactedHeaders_Test extends TestBase {
+
+       @Test void a01_sensitiveNamesDetectedCaseInsensitively() {
+               assertTrue(RedactedHeaders.isSensitive("Authorization"));
+               assertTrue(RedactedHeaders.isSensitive("authorization"));
+               assertTrue(RedactedHeaders.isSensitive("Cookie"));
+               assertTrue(RedactedHeaders.isSensitive("Set-Cookie"));
+       }
+
+       @Test void a02_nonSensitiveNamesPassThrough() {
+               assertFalse(RedactedHeaders.isSensitive("Accept"));
+               assertFalse(RedactedHeaders.isSensitive("Content-Type"));
+               assertFalse(RedactedHeaders.isSensitive(null));
+       }
+
+       @Test void a03_redactMasksSensitiveValues() {
+               assertEquals(RedactedHeaders.REDACTED, 
RedactedHeaders.redact("Authorization", "Bearer secret"));
+               assertEquals("text/plain", 
RedactedHeaders.redact("Content-Type", "text/plain"));
+       }
+
+       @Test void a04_customNameSet() {
+               var names = Set.of("X-Trace-Token");
+               assertEquals(RedactedHeaders.REDACTED, 
RedactedHeaders.redact("X-Trace-Token", "abc", names));
+               assertEquals("Bearer secret", 
RedactedHeaders.redact("Authorization", "Bearer secret", names));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackParser_MaxLength_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackParser_MaxLength_Test.java
new file mode 100644
index 0000000000..527c6e20d2
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackParser_MaxLength_Test.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.msgpack;
+
+import static org.apache.juneau.TestUtils.*;
+import static org.apache.juneau.commons.utils.StringUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Bounds-check tests for the wire-length maximum enforced by {@link 
MsgPackInputStream}.
+ *
+ * <p>
+ * A binary/string header carries a length prefix that is used to size a 
{@code byte[]} before any payload
+ * byte is read.  These tests confirm that an oversized declared length is 
rejected up front rather than
+ * driving a large allocation.
+ */
+class MsgPackParser_MaxLength_Test extends TestBase {
+
+       private static InputStream is(String spacedHex) {
+               return new ByteArrayInputStream(fromSpacedHex(spacedHex));
+       }
+
+       @Test void a01_binaryOversizedDeclaredLengthRejected() {
+               // bin32 header (0xC6) declaring 16 MiB + 1 bytes (0x01000001); 
a ~5-byte input must not size a giant buffer.
+               assertThrowsWithMessage(Exception.class, "exceeds maximum",
+                       () -> MsgPackParser.DEFAULT.parse(is("C6 01 00 00 01"), 
Object.class));
+       }
+
+       @Test void a02_stringOversizedDeclaredLengthRejected() {
+               // str32 header (0xDB) declaring 16 MiB + 1 bytes.
+               assertThrowsWithMessage(Exception.class, "exceeds maximum",
+                       () -> MsgPackParser.DEFAULT.parse(is("DB 01 00 00 01"), 
Object.class));
+       }
+
+       @Test void a03_binaryWithinCapAccepted() throws Exception {
+               // bin8 header (0xC4) declaring 2 bytes, followed by the 2 
payload bytes; parses normally within the cap.
+               var r = MsgPackParser.DEFAULT.parse(is("C4 02 61 62"), 
Object.class);
+               assertNotNull(r);
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/RestContext_ErrorPosture_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestContext_ErrorPosture_Test.java
new file mode 100644
index 0000000000..06d21b4f56
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestContext_ErrorPosture_Test.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.response.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Verifies the error-response posture: internal exception detail is only 
echoed back to the client when it is
+ * explicitly enabled (stack-trace rendering) or required by an RRPC dispatch 
target, and the plain-text error
+ * body only carries a message for application-authored exceptions.
+ */
+class RestContext_ErrorPosture_Test extends TestBase {
+
+       private static BasicHttpException ex() {
+               return new BadRequest(new 
RuntimeException("root-cause-detail"));
+       }
+
+       @Test void a01_thrownHeaderSuppressedByDefault() {
+               assertNull(RestContext.resolveThrownHeader(ex(), false, false));
+       }
+
+       @Test void a02_thrownHeaderWhenStackTracesRendered() {
+               assertNotNull(RestContext.resolveThrownHeader(ex(), true, 
false));
+       }
+
+       @Test void a03_thrownHeaderForRpcDispatch() {
+               assertNotNull(RestContext.resolveThrownHeader(ex(), false, 
true));
+       }
+
+       @Test void a04_appThrownMessageEchoed() {
+               var e = new BadRequest("visible message");
+               assertEquals("visible message", 
RestContext.suppressedErrorBodyMessage(e, true));
+       }
+
+       @Test void a05_nonAppThrownMessageSuppressed() {
+               assertEquals("", RestContext.suppressedErrorBodyMessage(ex(), 
false));
+       }
+
+       @Test void a06_appThrownMessageScrubbed() {
+               var e = new BadRequest("<b>x</b>&y");
+               var r = RestContext.suppressedErrorBodyMessage(e, true);
+               assertFalse(r.contains("<"));
+               assertFalse(r.contains(">"));
+               assertFalse(r.contains("&"));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/RestRequest_ContentParam_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestRequest_ContentParam_Test.java
new file mode 100644
index 0000000000..b11b57d9e2
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestRequest_ContentParam_Test.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Verifies that the {@code &content=} query-parameter override is only 
honored for the request methods that
+ * carry a body ({@code PUT}/{@code POST}), so a body can't be injected 
through the URL on a bodyless method.
+ */
+class RestRequest_ContentParam_Test extends TestBase {
+
+       @Test void a01_bodyMethodsAllowed() {
+               assertTrue(RestRequest.isContentParamMethod("PUT"));
+               assertTrue(RestRequest.isContentParamMethod("POST"));
+       }
+
+       @Test void a02_caseInsensitive() {
+               assertTrue(RestRequest.isContentParamMethod("put"));
+               assertTrue(RestRequest.isContentParamMethod("Post"));
+       }
+
+       @Test void a03_bodylessMethodsRejected() {
+               assertFalse(RestRequest.isContentParamMethod("GET"));
+               assertFalse(RestRequest.isContentParamMethod("DELETE"));
+               assertFalse(RestRequest.isContentParamMethod("HEAD"));
+               assertFalse(RestRequest.isContentParamMethod("OPTIONS"));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/RestResponse_SafeRedirect_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestResponse_SafeRedirect_Test.java
new file mode 100644
index 0000000000..21c0ccb479
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/RestResponse_SafeRedirect_Test.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Verifies the guard used by the safe-redirect helper: only relative paths 
and allow-listed hosts are treated
+ * as safe redirect targets, so a redirect target derived from request input 
can't send a user to an arbitrary
+ * external site.
+ */
+class RestResponse_SafeRedirect_Test extends TestBase {
+
+       @Test void a01_relativePathsAreSafe() {
+               assertTrue(RestResponse.isSafeRedirectUri("/app/page"));
+               assertTrue(RestResponse.isSafeRedirectUri("page"));
+               assertTrue(RestResponse.isSafeRedirectUri(""));
+               assertTrue(RestResponse.isSafeRedirectUri(null));
+       }
+
+       @Test void a02_protocolRelativeIsNotSafeWithoutAllowedHost() {
+               assertFalse(RestResponse.isSafeRedirectUri("//evil.example"));
+       }
+
+       @Test void a03_absoluteCrossOriginRejected() {
+               
assertFalse(RestResponse.isSafeRedirectUri("https://evil.example/steal";));
+       }
+
+       @Test void a04_absoluteAllowedHostAccepted() {
+               
assertTrue(RestResponse.isSafeRedirectUri("https://good.example/next";, 
"good.example"));
+               
assertTrue(RestResponse.isSafeRedirectUri("https://GOOD.example/next";, 
"good.example"));
+       }
+
+       @Test void a05_absoluteNonAllowedHostRejected() {
+               
assertFalse(RestResponse.isSafeRedirectUri("https://evil.example/steal";, 
"good.example"));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/Rest_PredefinedStatusCodes_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/Rest_PredefinedStatusCodes_Test.java
index 3e1503376b..8ff9b96977 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/Rest_PredefinedStatusCodes_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/Rest_PredefinedStatusCodes_Test.java
@@ -116,43 +116,43 @@ class Rest_PredefinedStatusCodes_Test extends TestBase {
                        .run()
                        .assertStatus(400)
                        .assertContent().isContains(
-                               "Unknown property 'f2' encountered while trying 
to parse into class"
+                               "Could not convert request content content to 
class type"
                        );
                b.put("/a?noTrace=true", "{f1:'foo', f2:'foo'}", 
APPLICATION_JSON)
                        .run()
                        .assertStatus(400)
                        .assertContent().isContains(
-                               "Unknown property 'f2' encountered while trying 
to parse into class"
+                               "Could not convert request content content to 
class type"
                        );
                b.put("/b?noTrace=true", "{f1:'foo'}", APPLICATION_JSON)
                        .run()
                        .assertStatus(400)
                        .assertContent().isContains(
-                               "NumberFormatException"
+                               "Could not convert request content content to 
class type"
                        );
                b.put("/c?noTrace=true", "{f1:1}", APPLICATION_JSON)
                        .run()
                        .assertStatus(400)
                        .assertContent().isContains(
-                               "could not be instantiated"
+                               "Could not convert request content content to 
class type"
                        );
                b.put("/d?noTrace=true", "{f1:1}", APPLICATION_JSON)
                        .run()
                        .assertStatus(400)
                        .assertContent().isContains(
-                               "could not be instantiated"
+                               "Could not convert request content content to 
class type"
                        );
                b.put("/e?noTrace=true", "{f1:1}", APPLICATION_JSON)
                        .run()
                        .assertStatus(400)
                        .assertContent().isContains(
-                               "Class is not public"
+                               "Could not convert request content content to 
class type"
                        );
                b.put("/f?noTrace=true", "'foo'", APPLICATION_JSON)
                        .run()
                        .assertStatus(400)
                        .assertContent().isContains(
-                               "Test error"
+                               "Could not convert request content content to 
class type"
                        );
                b.put("/g/123?noTrace=true&p1=foo", "'foo'", APPLICATION_JSON)
                        .run()
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/client/RedirectSecurity_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/client/RedirectSecurity_Test.java
new file mode 100644
index 0000000000..418b59c94a
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/client/RedirectSecurity_Test.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.client;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.net.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Verifies the credential-forwarding decision used when a client 
transparently follows a redirect: credentials
+ * are forwarded only to the exact same origin, and stripped on any origin 
change or scheme downgrade.
+ */
+class RedirectSecurity_Test extends TestBase {
+
+       private static URI u(String s) {
+               return URI.create(s);
+       }
+
+       @Test void a01_sameOriginForwards() {
+               
assertTrue(RedirectSecurity.sameOrigin(u("https://good.example/a";), 
u("https://good.example/b";)));
+               
assertFalse(RedirectSecurity.shouldStripCredentials(u("https://good.example/a";),
 u("https://good.example/b";)));
+       }
+
+       @Test void a02_sameOriginDefaultPortNormalized() {
+               
assertTrue(RedirectSecurity.sameOrigin(u("https://good.example/a";), 
u("https://good.example:443/b";)));
+       }
+
+       @Test void a03_differentHostStrips() {
+               
assertTrue(RedirectSecurity.shouldStripCredentials(u("https://good.example/a";), 
u("https://evil.example/b";)));
+       }
+
+       @Test void a04_differentPortStrips() {
+               
assertTrue(RedirectSecurity.shouldStripCredentials(u("https://good.example/a";), 
u("https://good.example:8443/b";)));
+       }
+
+       @Test void a05_schemeDowngradeStrips() {
+               
assertTrue(RedirectSecurity.isDowngrade(u("https://good.example/a";), 
u("http://good.example/b";)));
+               
assertTrue(RedirectSecurity.shouldStripCredentials(u("https://good.example/a";), 
u("http://good.example/b";)));
+       }
+
+       @Test void a06_stripSetIncludesCommonCredentialHeaders() {
+               var s = RedirectSecurity.stripOnCrossOrigin();
+               
assertTrue(s.stream().anyMatch("Authorization"::equalsIgnoreCase));
+               assertTrue(s.stream().anyMatch("Cookie"::equalsIgnoreCase));
+       }
+
+       @Test void a07_nonAbsoluteRejected() {
+               assertThrows(IllegalArgumentException.class, () -> 
RedirectSecurity.sameOrigin(u("/relative"), u("https://good.example/b";)));
+               assertThrows(IllegalArgumentException.class, () -> 
RedirectSecurity.sameOrigin(null, u("https://good.example/b";)));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/httppart/RequestContent_BufferSize_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/httppart/RequestContent_BufferSize_Test.java
new file mode 100644
index 0000000000..4db21e8f9a
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/httppart/RequestContent_BufferSize_Test.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.httppart;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Verifies that the reader buffer size derived from the client-supplied 
content length is clamped to the
+ * effective maximum-input ceiling, so a spoofed content length can't drive an 
oversized pre-allocation.
+ */
+class RequestContent_BufferSize_Test extends TestBase {
+
+       @Test void a01_absentLengthUsesDefault() {
+               assertEquals(8192, RequestContent.computeReaderBufferSize(0, 
1_000_000));
+               assertEquals(8192, RequestContent.computeReaderBufferSize(-1, 
1_000_000));
+       }
+
+       @Test void a02_smallLengthRaisedToDefault() {
+               assertEquals(8192, RequestContent.computeReaderBufferSize(100, 
1_000_000));
+       }
+
+       @Test void a03_lengthUsedWhenWithinCeiling() {
+               assertEquals(50_000, 
RequestContent.computeReaderBufferSize(50_000, 1_000_000));
+       }
+
+       @Test void a04_lengthClampedToCeiling() {
+               // A spoofed near-max content length must not size the buffer 
past the ceiling.
+               assertEquals(1_000_000, 
RequestContent.computeReaderBufferSize(Integer.MAX_VALUE, 1_000_000));
+       }
+
+       @Test void a05_noCeilingHonorsLength() {
+               assertEquals(Integer.MAX_VALUE, 
RequestContent.computeReaderBufferSize(Integer.MAX_VALUE, 0));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/httppart/RequestFormParams_MaxInput_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/httppart/RequestFormParams_MaxInput_Test.java
new file mode 100644
index 0000000000..1dc227488f
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/httppart/RequestFormParams_MaxInput_Test.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.httppart;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.response.*;
+import org.junit.jupiter.api.*;
+
+import jakarta.servlet.http.*;
+
+/**
+ * Verifies that a multipart request body is bounded by the effective 
maximum-input ceiling: it is rejected on
+ * the declared content length up front, and again on the cumulative 
parsed-part size as a backstop.
+ */
+class RequestFormParams_MaxInput_Test extends TestBase {
+
+       @Test void a01_declaredLengthWithinCeilingAccepted() {
+               RequestFormParams.checkMultipartLength(1_000_000, 500_000L);
+       }
+
+       @Test void a02_declaredLengthOverCeilingRejected() {
+               assertThrows(PayloadTooLarge.class, () -> 
RequestFormParams.checkMultipartLength(1_000_000, 2_000_000L));
+       }
+
+       @Test void a03_absentDeclaredLengthIgnored() {
+               RequestFormParams.checkMultipartLength(1_000_000, -1L);
+       }
+
+       @Test void a04_disabledCeilingSkipsDeclaredLengthCheck() {
+               RequestFormParams.checkMultipartLength(0, Long.MAX_VALUE);
+       }
+
+       @Test void a05_cumulativePartsWithinCeilingAccepted() {
+               RequestFormParams.checkMultipartLength(1_000_000, 
List.of(part(400_000), part(400_000)));
+       }
+
+       @Test void a06_cumulativePartsOverCeilingRejected() {
+               assertThrows(PayloadTooLarge.class,
+                       () -> RequestFormParams.checkMultipartLength(1_000_000, 
List.of(part(600_000), part(600_000))));
+       }
+
+       @Test void a07_emptyOrNullPartsIgnored() {
+               RequestFormParams.checkMultipartLength(1_000_000, 
(Collection<Part>)null);
+               RequestFormParams.checkMultipartLength(1_000_000, List.of());
+       }
+
+       private static Part part(long size) {
+               return new Part() {
+                       @Override public InputStream getInputStream() { throw 
new UnsupportedOperationException(); }
+                       @Override public String getContentType() { return null; 
}
+                       @Override public String getName() { return "f"; }
+                       @Override public String getSubmittedFileName() { return 
null; }
+                       @Override public long getSize() { return size; }
+                       @Override public void write(String fileName) { throw 
new UnsupportedOperationException(); }
+                       @Override public void delete() { throw new 
UnsupportedOperationException(); }
+                       @Override public String getHeader(String name) { return 
null; }
+                       @Override public Collection<String> getHeaders(String 
name) { return List.of(); }
+                       @Override public Collection<String> getHeaderNames() { 
return List.of(); }
+               };
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/swaps/ClassSwap_NoInitialize_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/swaps/ClassSwap_NoInitialize_Test.java
new file mode 100644
index 0000000000..111ed9c312
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/swaps/ClassSwap_NoInitialize_Test.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.swaps;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Verifies that resolving a {@link Class} value from parsed input does not 
trigger the named class's static
+ * initializer as a side effect.
+ */
+class ClassSwap_NoInitialize_Test extends TestBase {
+
+       /** Records whether its static initializer has run.  Referencing {@code 
.class} does not initialize it. */
+       static class SideEffect {
+               static volatile boolean initialized = false;
+               static { initialized = true; }
+       }
+
+       @Test void a01_unswapDoesNotInitializeResolvedClass() throws Exception {
+               SideEffect.initialized = false;
+               var swap = new ClassSwap();
+
+               var resolved = swap.unswap(null, SideEffect.class.getName(), 
null);
+
+               assertEquals(SideEffect.class, resolved);
+               assertFalse(SideEffect.initialized, "Resolving the class name 
must not run its static initializer");
+       }
+
+       @Test void a02_unswapResolvesCommonClass() throws Exception {
+               var swap = new ClassSwap();
+               assertEquals(String.class, swap.unswap(null, 
"java.lang.String", null));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/xml/XmlValidatingDtd_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/xml/XmlValidatingDtd_Test.java
new file mode 100644
index 0000000000..f80ce659e6
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/xml/XmlValidatingDtd_Test.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.xml;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Verifies that DTD processing stays disabled even when the {@code 
validating} parser option is enabled.
+ *
+ * <p>
+ * DTD support is not needed for data binding, so it is kept off regardless of 
the {@code validating} flag:
+ * a document declaring an internal DTD with nested entity references must be 
rejected rather than expanding
+ * those entities, closing the input-amplification surface that would 
otherwise be reachable by toggling
+ * {@code validating}.
+ */
+class XmlValidatingDtd_Test extends TestBase {
+
+       @Test void a01_internalDtdRejectedWhenValidatingEnabled() {
+               var p = XmlParser.create().validating().build();
+               var xml = "<?xml version=\"1.0\"?>"
+                       + "<!DOCTYPE A [<!ENTITY a \"aaaaaaaaaa\"><!ENTITY b 
\"&a;&a;&a;&a;&a;\">]>"
+                       + "<A>&b;</A>";
+
+               // DTD processing is disabled, so a document declaring an 
internal DTD with nested entity references is
+               // rejected rather than parsed — the input-amplification 
surface is never entered.
+               assertThrows(Exception.class, () -> p.parse(xml, 
JsonMap.class));
+       }
+
+       @Test void a03_internalDtdRejectedByDefaultParser() {
+               var p = XmlParser.DEFAULT;
+               var xml = "<?xml version=\"1.0\"?>"
+                       + "<!DOCTYPE A [<!ENTITY a \"aaaaaaaaaa\"><!ENTITY b 
\"&a;&a;&a;&a;&a;\">]>"
+                       + "<A>&b;</A>";
+
+               // Same guard on the default (non-validating) parser.
+               assertThrows(Exception.class, () -> p.parse(xml, 
JsonMap.class));
+       }
+
+       @Test void a02_doctypeRejectedWhenValidatingEnabled() {
+               var p = XmlParser.create().validating().build();
+               var xml = "<?xml version=\"1.0\"?>"
+                       + "<!DOCTYPE A [<!ELEMENT A ANY>]>"
+                       + "<A>x</A>";
+
+               assertThrows(Exception.class, () -> p.parse(xml, 
JsonMap.class));
+       }
+}

Reply via email to