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 52a7dd40a7 Resolve CodeQL code-scanning alerts: path-traversal, ReDoS, 
XXE, info-exposure hardening (TODO-235)
52a7dd40a7 is described below

commit 52a7dd40a7fa3aa6d3d6dc125de2a10fe20ef947
Author: James Bognar <[email protected]>
AuthorDate: Tue Jul 14 11:02:50 2026 -0400

    Resolve CodeQL code-scanning alerts: path-traversal, ReDoS, XXE, 
info-exposure hardening (TODO-235)
    
    - Path-injection: route juneau-sc GetConfiguration config lookups through 
FileUtils.resolveSafely and
      map traversal escape to 403 in LoadConfigResource; add PARSE-method 
traversal regression test for
      LogsResource.
    - ReDoS: fix polynomial-time regex in UrlPathMatcher, bound 
regex-validation input length in
      HttpPartSchema, and properly escape literal fragments in 
LogEntryFormatter's date/format templates.
    - XXE: disable DTD processing on XmlReader's non-validating path and harden 
the integration-test
      XmlValidatorParser; add XXE regression test.
    - Info exposure: AuthFilter returns a generic Unauthorized body (detail 
logged server-side);
      PlainTextPojoProcessor emits generic status text for Throwables in 
production (gated by
      renderResponseStackTraces).
    - Correctness: explicit narrowing cast in CharSequenceReader.
    
    Also dismissed 22 by-design/false-positive alerts (SSRF client transports, 
MsgPack spec narrowing,
    utility-level XSS/error-exposure FPs, and already-mitigated log-resource 
path-injection) with
    per-alert rationale recorded on GitHub.
    
    Co-authored-by: Cursor <[email protected]>
---
 .../juneau/commons/io/CharSequenceReader.java      |  2 +-
 .../juneau/marshall/httppart/HttpPartSchema.java   | 11 +++-
 .../org/apache/juneau/marshall/xml/XmlReader.java  |  3 +
 .../httppart/HttpPartSchema_Validation_Test.java   |  9 +++
 .../apache/juneau/marshall/xml/XmlXxe_Test.java    | 68 ++++++++++++++++++++++
 .../java/org/apache/juneau/XmlValidatorParser.java |  3 +
 .../rest/server/RestServer_ErrorExposure_Test.java | 59 +++++++++++++++++++
 .../juneau/rest/server/auth/AuthFilter_Test.java   | 40 +++++++++++--
 .../rest/server/util/UrlPathMatcher_Test.java      | 19 ++++++
 .../microservice/resources/LogEntryFormatter.java  | 48 ++++++++++++---
 .../resources/LogEntryFormatter_Test.java          | 27 +++++++++
 .../resources/LogsResource_PathTraversal_Test.java | 16 +++++
 .../apache/juneau/rest/server/auth/AuthFilter.java | 16 ++++-
 .../server/processor/PlainTextPojoProcessor.java   |  8 +++
 .../juneau/rest/server/util/UrlPathMatcher.java    |  2 +-
 .../server/config/repository/GetConfiguration.java | 19 +++---
 .../server/config/rest/LoadConfigResource.java     | 10 +++-
 17 files changed, 333 insertions(+), 27 deletions(-)

diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/CharSequenceReader.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/CharSequenceReader.java
index 5f8ccb3f5f..40d512243e 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/CharSequenceReader.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/CharSequenceReader.java
@@ -174,7 +174,7 @@ public class CharSequenceReader extends BufferedReader {
                        return 0;
                long n = Math.min((long) length - next, ns);
                n = Math.max(-next, n);
-               next += n;
+               next += (int) n;  // Safe narrowing: n is bounded to [-next, 
length-next], so next+n stays within int range [0, length].
                return n;
        }
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/httppart/HttpPartSchema.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/httppart/HttpPartSchema.java
index b6101be852..70fab81c14 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/httppart/HttpPartSchema.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/httppart/HttpPartSchema.java
@@ -4909,8 +4909,17 @@ public class HttpPartSchema {
                return pattern == null || pattern.matcher(x).matches();
        }
 
+       // Upper bound on the length of a value validated as a regular 
expression.  The value here is intentionally
+       // a caller-authored regex (JSON-Schema "format":"regex"), so it must 
be compiled rather than quoted; the
+       // compiled pattern is immediately discarded and never matched against 
anything.  This cap bounds the cost
+       // of that throwaway compilation so an over-long or deeply-nested 
pattern can't be used as a DoS vector.
+       private static final int MAX_REGEX_VALIDATION_LENGTH = 1000;
+
        private static boolean isValidRegex(String x) {
-               // ECMA-262 regex validation
+               // ECMA-262 regex validation.  Reject over-long values before 
compiling (the compiled Pattern is
+               // discarded and never used for matching, so bounding length is 
sufficient to cap the work done here).
+               if (x.length() > MAX_REGEX_VALIDATION_LENGTH)
+                       return false;
                try {
                        Pattern.compile(x);
                        return true;
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/xml/XmlReader.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/xml/XmlReader.java
index 1792978372..bc3696ef8e 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/xml/XmlReader.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/xml/XmlReader.java
@@ -66,6 +66,9 @@ 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 entirely on the 
non-validating path to close the residual XXE surface (e.g. billion-laughs and 
SYSTEM-entity attacks via a DOCTYPE).  DTD support is left enabled only when 
validation is explicitly requested, since DTD validation requires it.
+                       if (! validating)
+                               
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
                        if 
(factory.isPropertySupported(XMLInputFactory.REPORTER) && nn(reporter))
                                factory.setProperty(XMLInputFactory.REPORTER, 
reporter);
                        if 
(factory.isPropertySupported(XMLInputFactory.RESOLVER) && nn(resolver))
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/httppart/HttpPartSchema_Validation_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/httppart/HttpPartSchema_Validation_Test.java
index 5f5ec00ae1..0c27908aed 100644
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/httppart/HttpPartSchema_Validation_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/httppart/HttpPartSchema_Validation_Test.java
@@ -273,6 +273,15 @@ class HttpPartSchema_Validation_Test extends TestBase {
                assertThrowsWithMessage(SchemaValidationException.class, "Value 
does not match expected format", ()->s.validateInput("[unclosed"));
        }
 
+       @Test void a30b_format_regex_overLengthRejected() {
+               var s = 
HttpPartSchema.create().tString().noValidate().format("regex").build();
+               // A value at the validation cap that is a valid regex still 
validates.
+               assertDoesNotThrow(()->s.validateInput("a".repeat(1000)));
+               // A value longer than the cap is rejected without attempting 
compilation, bounding the work done here
+               // so an over-long/deeply-nested pattern cannot be used as a 
compile-time DoS vector.
+               assertThrowsWithMessage(SchemaValidationException.class, "Value 
does not match expected format", ()->s.validateInput("(".repeat(5000)));
+       }
+
        
//-----------------------------------------------------------------------------------------------------------------
        // DATE / DATE_TIME / DATE_TIME_ZONE format validation
        
//-----------------------------------------------------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/xml/XmlXxe_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/xml/XmlXxe_Test.java
new file mode 100644
index 0000000000..f17beeaea0
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/xml/XmlXxe_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.marshall.xml;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.file.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshall.json5.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.io.*;
+
+/**
+ * Verifies that {@link XmlReader} is not vulnerable to XML External Entity 
(XXE) attacks.
+ *
+ * <p>
+ * The default (non-validating) parser disables DTD processing entirely, so 
any document carrying a
+ * {@code <!DOCTYPE ...>} declaration (the vector for 
external/parameter-entity attacks) is rejected outright and no
+ * external entity is ever resolved.
+ */
+class XmlXxe_Test extends TestBase {
+
+       @Test void a01_externalEntityNotResolved(@TempDir Path tempDir) throws 
Exception {
+               var secret = tempDir.resolve("secret.txt");
+               Files.writeString(secret, "TOP-SECRET-CONTENTS");
+               var uri = secret.toUri().toString();
+
+               var xml = "<?xml version=\"1.0\"?>"
+                       + "<!DOCTYPE A [<!ENTITY xxe SYSTEM \"" + uri + "\">]>"
+                       + "<A>&xxe;</A>";
+
+               // DTD processing is disabled, so parsing must fail rather than 
resolving the external entity.
+               var e = assertThrows(Exception.class, () -> 
XmlParser.DEFAULT.parse(xml, Json5Map.class));
+
+               // The secret file contents must never appear anywhere in the 
failure.
+               assertFalse(e.toString().contains("TOP-SECRET-CONTENTS"), 
"External entity was resolved");
+       }
+
+       @Test void a02_doctypeRejected() {
+               var xml = "<?xml version=\"1.0\"?>"
+                       + "<!DOCTYPE A [<!ELEMENT A ANY>]>"
+                       + "<A>x</A>";
+
+               // Any DOCTYPE declaration is rejected on the non-validating 
path.
+               assertThrows(Exception.class, () -> 
XmlParser.DEFAULT.parse(xml, Json5Map.class));
+       }
+
+       @Test void a03_normalDocumentStillParses() throws Exception {
+               var xml = "<A b='1'><c>2</c></A>";
+               var m = XmlParser.DEFAULT.parse(xml, Json5Map.class);
+               assertEquals("{b:'1',c:'2'}", m.toString());
+       }
+}
diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/XmlValidatorParser.java
 
b/juneau-integration-tests/src/test/java/org/apache/juneau/XmlValidatorParser.java
index 031e138e9a..0797ebbd53 100755
--- 
a/juneau-integration-tests/src/test/java/org/apache/juneau/XmlValidatorParser.java
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/XmlValidatorParser.java
@@ -91,6 +91,9 @@ public class XmlValidatorParser extends XmlParser {
        protected XMLStreamReader getStaxReader(Reader in) throws Exception {
                var factory = XMLInputFactory.newInstance();
                factory.setProperty("javax.xml.stream.isNamespaceAware", false);
+               // This validator only checks well-formedness of serializer 
output (which never contains DTDs), so DTD processing and external-entity 
resolution are disabled to close the XXE surface.
+               factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
+               
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
                var parser = factory.createXMLStreamReader(in);
                parser.nextTag();
                return parser;
diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/RestServer_ErrorExposure_Test.java
 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/RestServer_ErrorExposure_Test.java
new file mode 100644
index 0000000000..2e90cbf53f
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/RestServer_ErrorExposure_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.rest.server;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Guards that unexpected server-side exception detail (message, class name, 
stack trace) is not leaked to
+ * HTTP clients in production, while remaining available in development when 
the
+ * {@code renderResponseStackTraces} flag is enabled.
+ */
+class RestServer_ErrorExposure_Test extends TestBase {
+
+       private static final String SECRET = "SECRET-DB-PASSWORD=hunter2";
+
+       // Production (default): renderResponseStackTraces is off.
+       @Rest
+       public static class A {
+               @RestGet public String boom() { throw new 
RuntimeException(SECRET); }
+       }
+
+       // Development: renderResponseStackTraces explicitly enabled.
+       @Rest(renderResponseStackTraces="true")
+       public static class B {
+               @RestGet public String boom() { throw new 
RuntimeException(SECRET); }
+       }
+
+       @Test void a01_productionMode_hidesInternalExceptionDetail() throws 
Exception {
+               var a = MockRestClient.buildLax(A.class);
+               var body = 
a.get("/boom").run().assertStatus(500).getContent().asString();
+               assertFalse(body.contains(SECRET), "Production body leaked the 
exception message: " + body);
+               assertFalse(body.contains("RuntimeException"), "Production body 
leaked the exception class name: " + body);
+               assertTrue(body.contains("Internal Server Error"), "Production 
body should carry only the generic status text: " + body);
+       }
+
+       @Test void a02_debugMode_rendersDetail() throws Exception {
+               var b = MockRestClient.buildLax(B.class);
+               var body = 
b.get("/boom").run().assertStatus(500).getContent().asString();
+               assertTrue(body.contains(SECRET), "Debug mode 
(renderResponseStackTraces=true) should render exception detail: " + body);
+       }
+}
diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/auth/AuthFilter_Test.java
 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/auth/AuthFilter_Test.java
index 5cb64ee053..8aad77a6d1 100644
--- 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/auth/AuthFilter_Test.java
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/auth/AuthFilter_Test.java
@@ -180,10 +180,6 @@ class AuthFilter_Test extends TestBase {
        }
 
        @Test void d03_doFilter_authException_nullMessage_stillWrites401() 
throws Exception {
-               // Even when constructed with a null message, 
BasicHttpException#getMessage() falls back
-               // to the status reason phrase ("Unauthorized") — so the {@code 
msg != null} branch in
-               // AuthFilter#sendChallenge is effectively unreachable through 
public API (the else-branch
-               // is dead code given BasicHttpException#getMessage() never 
returns null). // NOSONAR
                var f = new TestFilter(req -> { throw new 
AuthenticationException((String) null); });
                var req = MockServletRequest.create("GET", "/x");
                var resp = MockServletResponse.create();
@@ -195,6 +191,42 @@ class AuthFilter_Test extends TestBase {
                assertEquals(HttpServletResponse.SC_UNAUTHORIZED, 
resp.getStatus());
        }
 
+       // 
========================================================================
+       // sendChallenge — response body must be generic (no info leakage)
+       // 
========================================================================
+
+       @Test void d04_sendChallenge_writesGenericBody_notExceptionMessage() 
throws Exception {
+               var resp = mock(HttpServletResponse.class);
+               var sw = new StringWriter();
+               when(resp.getWriter()).thenReturn(new PrintWriter(sw));
+               // Sensitive detail that must NOT be echoed back to the client.
+               var e = new AuthenticationException("token signature invalid: 
secret-key-id=abc123")
+                       .wwwAuthenticate("Bearer realm=\"api\"");
+
+               AuthFilter.sendChallenge(resp, e);
+
+               verify(resp).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+               verify(resp).setHeader("WWW-Authenticate", "Bearer 
realm=\"api\"");
+               var body = sw.toString();
+               assertEquals("Unauthorized", body);
+               assertFalse(body.contains("secret-key-id"), "Auth-failure body 
must not leak the exception message");
+       }
+
+       @Test void d05_sendChallenge_wrappedCauseNotLeaked() throws Exception {
+               var resp = mock(HttpServletResponse.class);
+               var sw = new StringWriter();
+               when(resp.getWriter()).thenReturn(new PrintWriter(sw));
+               // A custom validator can wrap an internal exception whose 
message must not reach the client.
+               var e = new AuthenticationException(new 
IllegalStateException("jdbc://internal-host:5432 unreachable"));
+
+               AuthFilter.sendChallenge(resp, e);
+
+               verify(resp).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+               var body = sw.toString();
+               assertEquals("Unauthorized", body);
+               assertFalse(body.contains("internal-host"), "Wrapped cause 
message must not leak to the client");
+       }
+
        // 
========================================================================
        // doFilter — exceptions from the downstream chain propagate
        // 
========================================================================
diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/util/UrlPathMatcher_Test.java
 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/util/UrlPathMatcher_Test.java
index ca177e0dbc..d3b4cb3f2f 100644
--- 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/util/UrlPathMatcher_Test.java
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/util/UrlPathMatcher_Test.java
@@ -20,6 +20,7 @@ import static 
org.apache.juneau.rest.server.util.UrlPathMatcher.*;
 import static org.apache.juneau.test.bct.BctAssertions.*;
 import static org.junit.jupiter.api.Assertions.*;
 
+import java.time.*;
 import java.util.*;
 
 import org.apache.juneau.*;
@@ -357,4 +358,22 @@ class UrlPathMatcher_Test extends TestBase {
                check(p, "/*.*", "{}");
                shouldNotMatch(p, "/foo", "/foo", "/*", null);
        }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // ReDoS safety
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void j01_redos_pathologicalPatternCompilesQuickly() {
+               // Constructing a matcher computes a comparator string via a 
'\{[^{}]+\}' scan.  A pattern string
+               // consisting of many '{' characters previously drove that scan 
into O(n^2) backtracking; with the
+               // non-overlapping character class the construction stays 
effectively linear and must complete promptly.
+               var pattern = "/" + "{".repeat(200_000);
+               assertTimeoutPreemptively(Duration.ofSeconds(2), () -> 
of(pattern));
+       }
+
+       @Test void j02_redos_variablePatternComparatorUnchanged() {
+               // The comparator computation for well-formed variable patterns 
must be unaffected by the regex change.
+               assertEquals(of("/foo/{id}/bar").getComparator(), 
of("/foo/{name}/bar").getComparator());
+               assertEquals("/X/W/X/W/W", 
of("/foo/{id}/bar/{x}").getComparator());
+       }
 }
\ No newline at end of file
diff --git 
a/juneau-microservice/juneau-microservice/src/main/java/org/apache/juneau/microservice/resources/LogEntryFormatter.java
 
b/juneau-microservice/juneau-microservice/src/main/java/org/apache/juneau/microservice/resources/LogEntryFormatter.java
index db58a1105e..01e3fd66f7 100644
--- 
a/juneau-microservice/juneau-microservice/src/main/java/org/apache/juneau/microservice/resources/LogEntryFormatter.java
+++ 
b/juneau-microservice/juneau-microservice/src/main/java/org/apache/juneau/microservice/resources/LogEntryFormatter.java
@@ -62,6 +62,36 @@ public class LogEntryFormatter extends Formatter {
                return Integer.toHexString(i);
        }
 
+       // Appends a single character to the log-parsing regex as a literal, 
escaping it when it is a regex
+       // metacharacter.  The format and date-format strings are 
caller-supplied configuration, so their
+       // literal characters must be escaped to keep them matched verbatim and 
prevent regex-injection.
+       private static void appendLiteral(StringBuilder re, char c) {
+               if (! (Character.isLetterOrDigit(c) || 
Character.isWhitespace(c)))
+                       re.append('\\');
+               re.append(c);
+       }
+
+       // Appends each character of a string to the log-parsing regex as a 
literal (see {@link #appendLiteral(StringBuilder, char)}).
+       private static void appendLiteral(StringBuilder re, String s) {
+               for (var i = 0; i < s.length(); i++)
+                       appendLiteral(re, s.charAt(i));
+       }
+
+       // Converts a SimpleDateFormat pattern into a regex fragment that 
matches formatted dates: date/time pattern
+       // letters become "\d" matchers and all other characters are treated as 
regex literals.  Escaping the
+       // non-letter characters (rather than only ".") prevents 
regex-injection from a caller-supplied date format.
+       private static String dateFormatToRegex(String dateFormat) {
+               var re = new StringBuilder();
+               for (var i = 0; i < dateFormat.length(); i++) {
+                       var c = dateFormat.charAt(i);
+                       if ("mHhsSdMy".indexOf(c) >= 0)
+                               re.append("\\d");
+                       else
+                               appendLiteral(re, c);
+               }
+               return re.toString();
+       }
+
        private ConcurrentHashMap<String,AtomicInteger> hashes;
        private DateFormat df;
        private String format;
@@ -135,17 +165,15 @@ public class LogEntryFormatter extends Formatter {
                        if (state == S1) {
                                if (c == '%')
                                        state = S2;
-                               else {
-                                       if (! (Character.isLetterOrDigit(c) || 
Character.isWhitespace(c)))
-                                               re.append('\\');
-                                       re.append(c);
-                               }
+                               else
+                                       appendLiteral(re, c);
                        } else if (state == S2) {
                                if (Character.isDigit(c)) {
                                        i1 = i;
                                        state = S3;
                                } else {
-                                       re.append("\\%").append(c);
+                                       re.append("\\%");
+                                       appendLiteral(re, c);
                                        state = S1;
                                }
                        } else if (state == S3) {  // NOSONAR - State check 
necessary for state machine
@@ -155,7 +183,8 @@ public class LogEntryFormatter extends Formatter {
                                        // Stay in S3: group numbers may be 
multi-digit (e.g. %10$s for {spanId}).
                                        state = S3;  // NOSONAR - explicit 
self-transition documents the multi-digit case
                                } else {
-                                       
re.append("\\%").append(format.substring(i1, i)); // HTT - requires %digit 
followed by non-$ which can't come from standard format placeholders
+                                       re.append("\\%");
+                                       appendLiteral(re, format.substring(i1, 
i)); // HTT - requires %digit followed by non-$ which can't come from standard 
format placeholders
                                        state = S1;
                                }
                        } else if (state == S4) {
@@ -164,7 +193,7 @@ public class LogEntryFormatter extends Formatter {
                                        switch (group) {
                                                case 1:
                                                        
fieldIndexes.put("date", index++);
-                                                       
re.append("(").append(dateFormat.replaceAll("[mHhsSdMy]", "\\\\d").replace(".", 
"\\.")).append(")");
+                                                       
re.append("(").append(dateFormatToRegex(dateFormat)).append(")");
                                                        break;
                                                case 2:
                                                        
fieldIndexes.put("class", index++);
@@ -205,7 +234,8 @@ public class LogEntryFormatter extends Formatter {
                                                default: // HTT - group numbers 
> 10 would require a format placeholder beyond {spanId} // NOSONAR
                                        }
                                } else {
-                                       
re.append("\\%").append(format.substring(i1, i)); // HTT - requires %digit$ 
followed by non-s which can't come from standard format placeholders
+                                       re.append("\\%");
+                                       appendLiteral(re, format.substring(i1, 
i)); // HTT - requires %digit$ followed by non-s which can't come from standard 
format placeholders
                                }
                                state = S1;
                        }
diff --git 
a/juneau-microservice/juneau-microservice/src/test/java/org/apache/juneau/microservice/resources/LogEntryFormatter_Test.java
 
b/juneau-microservice/juneau-microservice/src/test/java/org/apache/juneau/microservice/resources/LogEntryFormatter_Test.java
index b057056311..1c8b29540e 100644
--- 
a/juneau-microservice/juneau-microservice/src/test/java/org/apache/juneau/microservice/resources/LogEntryFormatter_Test.java
+++ 
b/juneau-microservice/juneau-microservice/src/test/java/org/apache/juneau/microservice/resources/LogEntryFormatter_Test.java
@@ -227,4 +227,31 @@ class LogEntryFormatter_Test extends TestBase {
                assertEquals("", TraceContext.currentTraceId());
                assertEquals("", TraceContext.currentSpanId());
        }
+
+       // 
-----------------------------------------------------------------------------------------------------------------
+       // Regex-injection safety - the caller-supplied format/dateFormat 
strings are escaped into the parsing regex.
+       // 
-----------------------------------------------------------------------------------------------------------------
+
+       @Test void c01_dateFormat_regexMetacharsEscapedAndMatch() {
+               // A date format containing regex metacharacters must be 
escaped (not injected) yet still match its own
+               // formatted output.  With the previous partial escaping, 
"(MM)" and "[dd]" produced a stray capture
+               // group and character class, so the resulting pattern no 
longer matched the formatted date.
+               var f = new LogEntryFormatter("[{date}] {msg}", "yyyy(MM)[dd]", 
false);
+               var r = new LogRecord(Level.INFO, "hello");
+               var line = f.format(r);
+               var m = f.getLogEntryPattern().matcher(line);
+               assertTrue(m.matches(), "Pattern should match its own formatted 
output with a metacharacter date format, got:\n" + line);
+       }
+
+       @Test void c02_dateFormat_unbalancedMetacharsDoNotThrow() {
+               // An unbalanced regex metacharacter in the date format 
previously produced an invalid pattern that
+               // threw a PatternSyntaxException during construction; it must 
now be escaped and compile cleanly.
+               assertDoesNotThrow(() -> new LogEntryFormatter("[{date}] 
{msg}", "yyyy(((MM", false));
+       }
+
+       @Test void c03_format_regexMetacharsEscaped() {
+               // Regex metacharacters in the literal portions of the format 
string must be escaped, so an adversarial
+               // format cannot inject regex syntax or break compilation of 
the log-parsing pattern.
+               assertDoesNotThrow(() -> new 
LogEntryFormatter("(([{date}]{level})+ {msg}%n", DATE_FORMAT, false));
+       }
 }
diff --git 
a/juneau-microservice/juneau-microservice/src/test/java/org/apache/juneau/microservice/resources/LogsResource_PathTraversal_Test.java
 
b/juneau-microservice/juneau-microservice/src/test/java/org/apache/juneau/microservice/resources/LogsResource_PathTraversal_Test.java
index c306090032..5e3328ca7f 100644
--- 
a/juneau-microservice/juneau-microservice/src/test/java/org/apache/juneau/microservice/resources/LogsResource_PathTraversal_Test.java
+++ 
b/juneau-microservice/juneau-microservice/src/test/java/org/apache/juneau/microservice/resources/LogsResource_PathTraversal_Test.java
@@ -291,4 +291,20 @@ class LogsResource_PathTraversal_Test extends TestBase {
                                "Symlink-escape response must not leak the 
outside-root secret");
                }
        }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // PARSE traversal — the remaining operation surface 
(view/download/delete covered above)
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test void t13_methodPARSE_traversal_returns403() throws Exception {
+               try (var c = buildClient()) {
+                       @SuppressWarnings({
+                               "resource"  // Closeable resources in tests are 
intentionally unassigned; closing is handled by test infrastructure.
+                       })
+                       var resp = 
c.get("/../outside-secret.log?method=PARSE").run();
+                       assertEquals(403, resp.getStatusCode(), "GET 
/../outside-secret.log?method=PARSE must be rejected");
+                       
assertFalse(resp.getContent().asString().contains("AUDIT_OUTSIDE_LOG_SECRET"),
+                               "Response body must not leak the outside-root 
secret");
+               }
+       }
 }
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/auth/AuthFilter.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/auth/AuthFilter.java
index fe83ccd45d..628b10e7c4 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/auth/AuthFilter.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/auth/AuthFilter.java
@@ -18,6 +18,8 @@ package org.apache.juneau.rest.server.auth;
 
 import java.io.*;
 import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import jakarta.servlet.*;
 import jakarta.servlet.http.*;
@@ -61,6 +63,11 @@ public abstract class AuthFilter implements Filter, 
Authenticator {
        /** WWW-Authenticate response header name (RFC 7235 §4.1). */
        static final String WWW_AUTHENTICATE = "WWW-Authenticate";
 
+       private static final Logger LOG = 
Logger.getLogger(AuthFilter.class.getName());
+
+       /** Generic {@code 401} response body — deliberately reveals no failure 
detail to the client. */
+       static final String GENERIC_UNAUTHORIZED_MESSAGE = "Unauthorized";
+
        /**
         * Standalone-filter entry point.
         *
@@ -124,8 +131,11 @@ public abstract class AuthFilter implements Filter, 
Authenticator {
                        .findFirst()
                        .ifPresent(v -> resp.setHeader(WWW_AUTHENTICATE, v));
                resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-               var msg = e.getMessage();
-               if (msg != null)
-                       resp.getWriter().write(msg);
+               // Return a generic body to the client.  The exception message 
can carry internal detail from a
+               // custom validator (e.g. a wrapped cause), so it must not be 
echoed over the wire — the
+               // WWW-Authenticate challenge set above is the only 
auth-specific data a client needs.  Detailed
+               // failure information is logged server-side for diagnostics.
+               LOG.log(Level.FINE, e, () -> "Authentication failed.");
+               resp.getWriter().write(GENERIC_UNAUTHORIZED_MESSAGE);
        }
 }
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/processor/PlainTextPojoProcessor.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/processor/PlainTextPojoProcessor.java
index b2a2356ee5..c29b37e223 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/processor/PlainTextPojoProcessor.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/processor/PlainTextPojoProcessor.java
@@ -58,6 +58,14 @@ public class PlainTextPojoProcessor implements 
ResponseProcessor {
                        w.append("null");
                } else if (o instanceof Map || o instanceof Collection || 
o.getClass().isArray()) {
                        w.append(Json5.of(o));
+               } else if (o instanceof Throwable && ! 
opSession.getRestContext().isRenderResponseStackTraces()) {
+                       // A thrown exception reached the plain-text renderer 
as the response content.  Emitting its
+                       // toString() (class name + message) would leak 
internal detail to the client, so in production
+                       // we write only the generic HTTP status text.  
Structured detail remains in the 'Thrown'
+                       // response header and server-side logs; enable 
renderResponseStackTraces for full in-response
+                       // detail during development.
+                       var statusText = 
RestUtils.getHttpResponseText(res.getStatus());
+                       w.append(statusText == null ? "" : statusText);
                } else {
                        
w.append(req.getMarshallingSession().getClassMetaForObject(o).toString(o));
                }
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/UrlPathMatcher.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/UrlPathMatcher.java
index 6c34cc1688..f329b81957 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/UrlPathMatcher.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/UrlPathMatcher.java
@@ -98,7 +98,7 @@ public abstract sealed class UrlPathMatcher implements 
Comparable<UrlPathMatcher
                                this.pattern = patternString;
                        }
 
-                       var c = patternString.replaceAll("\\{[^\\}]+\\}", 
".").replaceAll("\\w+", "X").replace(".", "W");
+                       var c = patternString.replaceAll("\\{[^{}]+\\}", 
".").replaceAll("\\w+", "X").replace(".", "W");
                        if (c.isEmpty())
                                c = "+";
                        if (! c.endsWith("/*"))
diff --git 
a/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/repository/GetConfiguration.java
 
b/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/repository/GetConfiguration.java
index e84cccecd4..7d07daf8d1 100644
--- 
a/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/repository/GetConfiguration.java
+++ 
b/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/repository/GetConfiguration.java
@@ -16,6 +16,8 @@
  */
 package org.apache.juneau.server.config.repository;
 
+import static org.apache.juneau.commons.utils.FileUtils.*;
+
 import java.io.*;
 import java.nio.file.*;
 import java.util.*;
@@ -34,7 +36,6 @@ public class GetConfiguration implements Command, 
GetValue<Map<String,ConfigItem
        private static final String APPLICATION = "APPLICATION";
        private static final String PROJECT = "PROJECT";
        private static final String EXT = ".cfg";
-       private static final String BAR = "/";
 
        private Map<String,ConfigItem> configs = new HashMap<>();
 
@@ -67,9 +68,11 @@ public class GetConfiguration implements Command, 
GetValue<Map<String,ConfigItem
 
                var gitControl = new GitControl(pathStr, git);
 
-               var path = new File(pathStr);
+               // Trusted operator-configured local git checkout directory 
(GitServer/pathLocal).
+               // Serves as the confinement root for the project/application 
config file lookups below.
+               var root = new File(pathStr);
 
-               if (path.isDirectory()) {
+               if (root.isDirectory()) {
                        gitControl.pullFromRepo();
                } else {
                        gitControl.cloneRepo();
@@ -81,14 +84,16 @@ public class GetConfiguration implements Command, 
GetValue<Map<String,ConfigItem
                var fileDefaultStr = APPLICATION.toLowerCase().concat(EXT);
                var fileProjectStr = this.project.concat(EXT);
 
-               var fileDefault = new 
File(pathStr.concat(BAR).concat(fileDefaultStr));
-               if (fileDefault.exists()) {
+               // Resolve the config file names under the checkout root 
through the shared boundary check
+               // so a crafted project name cannot escape the repo directory 
(empty = file absent).
+               var fileDefault = resolveSafely(root, 
fileDefaultStr).orElse(null);
+               if (fileDefault != null) {
                        var lines = new 
String(Files.readAllBytes(fileDefault.toPath()));
                        configs.put(APPLICATION, new ConfigItem(lines));
                }
 
-               var fileProject = new 
File(pathStr.concat(BAR).concat(fileProjectStr));
-               if (fileProject.exists()) {
+               var fileProject = resolveSafely(root, 
fileProjectStr).orElse(null);
+               if (fileProject != null) {
                        var linesProject = new 
String(Files.readAllBytes(fileProject.toPath()));
                        configs.put(PROJECT, new ConfigItem(linesProject));
                }
diff --git 
a/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/rest/LoadConfigResource.java
 
b/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/rest/LoadConfigResource.java
index 0816e519e6..c1c1a504d5 100644
--- 
a/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/rest/LoadConfigResource.java
+++ 
b/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/rest/LoadConfigResource.java
@@ -17,6 +17,7 @@
 package org.apache.juneau.server.config.rest;
 
 import org.apache.juneau.http.*;
+import org.apache.juneau.http.response.*;
 import org.apache.juneau.marshall.json.*;
 import org.apache.juneau.rest.server.*;
 import org.apache.juneau.rest.server.servlet.*;
@@ -47,7 +48,14 @@ public class LoadConfigResource extends RestServlet {
                var jsonSerializer = JsonSerializer.DEFAULT_READABLE;
 
                var config = new GetConfiguration(project, branch);
-               config.execute();
+               try {
+                       config.execute();
+               } catch (@SuppressWarnings("unused") IllegalArgumentException 
e) {
+                       // A crafted project/branch that escapes the checkout 
root is rejected by
+                       // FileUtils.resolveSafely(). Map that boundary 
violation to 403 with a generic
+                       // message so the offending path is never echoed back 
to the client.
+                       throw new Forbidden("Access denied.");
+               }
 
                return jsonSerializer.serialize(config.get());
        }


Reply via email to