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

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/main by this push:
     new 54a1ca3ff WW-5752 Normalize request-derived values in log messages 
(#1982)
54a1ca3ff is described below

commit 54a1ca3ff61c40f0c9e26dacf81c0823ec4809fa
Author: ⳕⲛτⲉⲅⲥⲉⳏτⲟⲅ 🕵🏻 <[email protected]>
AuthorDate: Fri Oct 2 11:23:42 2026 +0530

    WW-5752 Normalize request-derived values in log messages (#1982)
    
    * WW-5752 Normalize request-derived values in log messages
    
    Request-derived strings were logged unchanged, so a decoded line break in a 
URL path, parameter name, locale or JSON key produced forged log lines. Apply 
normalizeSpace, as TokenHelper, the multipart classes and ParametersInterceptor 
already do.
    
    * WW-5752 Normalize the token name in the missing-token debug message
    
    Review follow-up: validToken() still logged the request-supplied token name 
unchanged.
    
    ---------
    
    Co-authored-by: Gouri Sankar A <[email protected]>
---
 .../struts2/action/DefaultCspReportAction.java     |  4 +-
 .../dispatcher/mapper/DefaultActionMapper.java     |  8 +--
 .../dispatcher/mapper/RestfulActionMapper.java     |  4 +-
 .../parameter/ParametersInterceptor.java           | 20 +++----
 .../struts2/locale/DefaultLocaleProvider.java      |  5 +-
 .../java/org/apache/struts2/util/TokenHelper.java  |  4 +-
 .../apache/struts2/action/CspReportActionTest.java | 12 +++++
 .../dispatcher/mapper/DefaultActionMapperTest.java | 17 ++++++
 .../dispatcher/mapper/RestfulActionMapperTest.java | 12 +++++
 .../parameter/ParametersInterceptorTest.java       | 22 ++++++++
 .../struts2/locale/DefaultLocaleProviderTest.java  | 16 ++++++
 .../java/org/apache/struts2/test/LogCapture.java   | 62 ++++++++++++++++++++++
 .../org/apache/struts2/util/TokenHelperTest.java   | 39 ++++++++++++++
 .../org/apache/struts2/json/JSONInterceptor.java   | 20 +++----
 .../apache/struts2/json/JSONInterceptorTest.java   | 49 +++++++++++++++++
 15 files changed, 266 insertions(+), 28 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/action/DefaultCspReportAction.java 
b/core/src/main/java/org/apache/struts2/action/DefaultCspReportAction.java
index 3bae9f0ee..9b71c15ad 100644
--- a/core/src/main/java/org/apache/struts2/action/DefaultCspReportAction.java
+++ b/core/src/main/java/org/apache/struts2/action/DefaultCspReportAction.java
@@ -21,6 +21,8 @@ package org.apache.struts2.action;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+import static org.apache.commons.lang3.StringUtils.normalizeSpace;
+
 /**
  * The default implementation of {@link CspReportAction} that simply logs the 
JSON object
  * that contains the details of the CSP violation.
@@ -33,6 +35,6 @@ public class DefaultCspReportAction extends CspReportAction {
 
     @Override
     void processReport(String jsonCspReport) {
-        LOG.error(jsonCspReport);
+        LOG.error(normalizeSpace(jsonCspReport));
     }
 }
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java
index 6e0f57c8b..60d31be68 100644
--- 
a/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java
@@ -42,6 +42,8 @@ import java.util.Map;
 import java.util.Set;
 import java.util.regex.Pattern;
 
+import static org.apache.commons.lang3.StringUtils.normalizeSpace;
+
 /**
  * <!-- START SNIPPET: javadoc -->
  * <p>
@@ -410,7 +412,7 @@ public class DefaultActionMapper implements ActionMapper {
         } else {
             LOG.warn(
                 "{} did not match allowed namespace names {} - default 
namespace {} will be used!",
-                rawNamespace, allowedNamespaceNames, defaultNamespaceName
+                normalizeSpace(rawNamespace), allowedNamespaceNames, 
defaultNamespaceName
             );
             return defaultNamespaceName;
         }
@@ -426,7 +428,7 @@ public class DefaultActionMapper implements ActionMapper {
         if (allowedActionNames.matcher(rawActionName).matches()) {
             return rawActionName;
         } else {
-            LOG.warn("{} did not match allowed action names {} - default 
action {} will be used!", rawActionName, allowedActionNames, defaultActionName);
+            LOG.warn("{} did not match allowed action names {} - default 
action {} will be used!", normalizeSpace(rawActionName), allowedActionNames, 
defaultActionName);
             return defaultActionName;
         }
     }
@@ -441,7 +443,7 @@ public class DefaultActionMapper implements ActionMapper {
         if (allowedMethodNames.matcher(rawMethodName).matches()) {
             return rawMethodName;
         } else {
-            LOG.warn("{} did not match allowed method names {} - default 
method {} will be used!", rawMethodName, allowedMethodNames, defaultMethodName);
+            LOG.warn("{} did not match allowed method names {} - default 
method {} will be used!", normalizeSpace(rawMethodName), allowedMethodNames, 
defaultMethodName);
             return defaultMethodName;
         }
     }
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java
index a9a929592..f38ecfecf 100644
--- 
a/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java
@@ -32,6 +32,8 @@ import java.util.Map;
 import java.util.StringTokenizer;
 import java.util.regex.Pattern;
 
+import static org.apache.commons.lang3.StringUtils.normalizeSpace;
+
 /**
  * Simple Restfull Action Mapper to support REST application
  * See docs for more information
@@ -133,7 +135,7 @@ public class RestfulActionMapper implements ActionMapper {
         if (allowedActionNames.matcher(rawActionName).matches()) {
             return rawActionName;
         } else {
-            LOG.warn("{} did not match allowed action names {} - default 
action {} will be used!", rawActionName, allowedActionNames, defaultActionName);
+            LOG.warn("{} did not match allowed action names {} - default 
action {} will be used!", normalizeSpace(rawActionName), allowedActionNames, 
defaultActionName);
             return defaultActionName;
         }
     }
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
 
b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
index 22d575eab..254e471cc 100644
--- 
a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
@@ -634,9 +634,9 @@ public class ParametersInterceptor extends 
MethodFilterInterceptor {
                 LOG.warn("Parameter [{}] is too long, allowed length is [{}]. 
Use Interceptor Parameter Overriding " +
                         "to override the limit, see more at\n" +
                         
"https://struts.apache.org/core-developers/interceptors.html#interceptor-parameter-overriding";,
-                    name, paramNameMaxLength);
+                    normalizeSpace(name), paramNameMaxLength);
             } else {
-                LOG.warn("Parameter [{}] is too long, allowed length is [{}]", 
name, paramNameMaxLength);
+                LOG.warn("Parameter [{}] is too long, allowed length is [{}]", 
normalizeSpace(name), paramNameMaxLength);
             }
         }
         return matchLength;
@@ -648,9 +648,9 @@ public class ParametersInterceptor extends 
MethodFilterInterceptor {
             if (devMode) {
                 LOG.warn("Parameter [{}] didn't match accepted pattern [{}]! 
See Accepted / Excluded patterns at\n" +
                          
"https://struts.apache.org/security/#accepted--excluded-patterns";,
-                        paramName, result.getAcceptedPattern());
+                        normalizeSpace(paramName), 
result.getAcceptedPattern());
             } else {
-                LOG.debug("Parameter [{}] didn't match accepted pattern 
[{}]!", paramName, result.getAcceptedPattern());
+                LOG.debug("Parameter [{}] didn't match accepted pattern 
[{}]!", normalizeSpace(paramName), result.getAcceptedPattern());
             }
             return false;
         }
@@ -663,9 +663,9 @@ public class ParametersInterceptor extends 
MethodFilterInterceptor {
             if (devMode) {
                 LOG.warn("Parameter [{}] matches excluded pattern [{}]! See 
Accepted / Excluded patterns at\n" +
                          
"https://struts.apache.org/security/#accepted--excluded-patterns";,
-                        paramName, result.getExcludedPattern());
+                        normalizeSpace(paramName), 
result.getExcludedPattern());
             } else {
-                LOG.debug("Parameter [{}] matches excluded pattern [{}]!", 
paramName, result.getExcludedPattern());
+                LOG.debug("Parameter [{}] matches excluded pattern [{}]!", 
normalizeSpace(paramName), result.getExcludedPattern());
             }
             return true;
         }
@@ -682,9 +682,9 @@ public class ParametersInterceptor extends 
MethodFilterInterceptor {
                 if (devMode) {
                     LOG.warn("Parameter value [{}] matches excluded pattern 
[{}]! See Accepting/Excluding parameter values at\n" +
                              
"https://struts.apache.org/core-developers/parameters-interceptor#excluding-parameter-values";,
-                            value, excludedValuePatterns);
+                            normalizeSpace(value), excludedValuePatterns);
                 } else {
-                    LOG.debug("Parameter value [{}] matches excluded pattern 
[{}]", value, excludedValuePattern);
+                    LOG.debug("Parameter value [{}] matches excluded pattern 
[{}]", normalizeSpace(value), excludedValuePattern);
                 }
                 return true;
             }
@@ -705,9 +705,9 @@ public class ParametersInterceptor extends 
MethodFilterInterceptor {
         if (devMode) {
             LOG.warn("Parameter value [{}] didn't match accepted pattern [{}]! 
See Accepting/Excluding parameter values at\n" +
                      
"https://struts.apache.org/core-developers/parameters-interceptor#excluding-parameter-values";,
-                    value, acceptedValuePatterns);
+                    normalizeSpace(value), acceptedValuePatterns);
         } else {
-            LOG.debug("Parameter value [{}] was not accepted!", value);
+            LOG.debug("Parameter value [{}] was not accepted!", 
normalizeSpace(value));
         }
         return false;
     }
diff --git 
a/core/src/main/java/org/apache/struts2/locale/DefaultLocaleProvider.java 
b/core/src/main/java/org/apache/struts2/locale/DefaultLocaleProvider.java
index 2cf266fea..3301a8ca6 100644
--- a/core/src/main/java/org/apache/struts2/locale/DefaultLocaleProvider.java
+++ b/core/src/main/java/org/apache/struts2/locale/DefaultLocaleProvider.java
@@ -22,11 +22,12 @@ import org.apache.commons.lang3.LocaleUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.message.ParameterizedMessage;
 import org.apache.struts2.ActionContext;
 
 import java.util.Locale;
 
+import static org.apache.commons.lang3.StringUtils.normalizeSpace;
+
 /**
  * Default implementation of {@link LocaleProvider}
  */
@@ -62,7 +63,7 @@ public class DefaultLocaleProvider implements LocaleProvider {
         try {
             locale = LocaleUtils.toLocale(StringUtils.trimToNull(localeStr));
         } catch (IllegalArgumentException e) {
-            LOG.warn(new ParameterizedMessage("Cannot convert [{}] to proper 
locale", localeStr), e);
+            LOG.warn("Cannot convert [{}] to proper locale", 
normalizeSpace(localeStr));
         }
         return locale;
     }
diff --git a/core/src/main/java/org/apache/struts2/util/TokenHelper.java 
b/core/src/main/java/org/apache/struts2/util/TokenHelper.java
index 42ba4cc60..8bc349e91 100644
--- a/core/src/main/java/org/apache/struts2/util/TokenHelper.java
+++ b/core/src/main/java/org/apache/struts2/util/TokenHelper.java
@@ -133,7 +133,7 @@ public class TokenHelper {
         Parameter parameter = params.get(tokenName);
 
         if (!parameter.isDefined()) {
-            LOG.warn("Could not find token mapped to token name: {}", 
tokenName);
+            LOG.warn("Could not find token mapped to token name: {}", 
normalizeSpace(tokenName));
             return null;
         }
         return parameter.getValue();
@@ -177,7 +177,7 @@ public class TokenHelper {
         String token = getToken(tokenName);
 
         if (token == null) {
-            LOG.debug("No token found for token name {} -> Invalid token ", 
tokenName);
+            LOG.debug("No token found for token name {} -> Invalid token ", 
normalizeSpace(tokenName));
             return false;
         }
 
diff --git 
a/core/src/test/java/org/apache/struts2/action/CspReportActionTest.java 
b/core/src/test/java/org/apache/struts2/action/CspReportActionTest.java
index 6485454fb..cbfaeeb20 100644
--- a/core/src/test/java/org/apache/struts2/action/CspReportActionTest.java
+++ b/core/src/test/java/org/apache/struts2/action/CspReportActionTest.java
@@ -19,6 +19,7 @@
 package org.apache.struts2.action;
 
 import org.apache.struts2.XWorkTestCase;
+import org.apache.struts2.test.LogCapture;
 import org.apache.struts2.interceptor.csp.CspSettings;
 import org.springframework.http.HttpMethod;
 import org.springframework.mock.web.MockHttpServletRequest;
@@ -48,6 +49,17 @@ public class CspReportActionTest extends XWorkTestCase {
     }
   }
 
+  public void testDefaultActionDoesNotInjectLineBreaksIntoLog() {
+    try (LogCapture logs = new LogCapture(DefaultCspReportAction.class)) {
+      new 
DefaultCspReportAction().processReport("{\"csp-report\":{}}\n12:00:00 ERROR 
forged");
+
+      assertEquals(1, logs.messages().size());
+      String message = logs.messages().get(0);
+      assertTrue(message, message.contains("12:00:00 ERROR forged"));
+      assertFalse(message, message.contains("\n") || message.contains("\r"));
+    }
+  }
+
   public void testWhenNoContentLength_thenReportNotProcessed() {
     TestCspReportAction cspReportAction = new TestCspReportAction();
     MockHttpServletRequest request = new MockHttpServletRequest("POST", 
"/requestUri");
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapperTest.java
 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapperTest.java
index c047ec433..a03319c3a 100644
--- 
a/core/src/test/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapperTest.java
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapperTest.java
@@ -25,6 +25,7 @@ import java.util.Map;
 
 import org.apache.struts2.ServletActionContext;
 import org.apache.struts2.StrutsInternalTestCase;
+import org.apache.struts2.test.LogCapture;
 import org.apache.struts2.views.jsp.StrutsMockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletRequest;
 
@@ -865,6 +866,22 @@ public class DefaultActionMapperTest extends 
StrutsInternalTestCase {
         assertEquals("/test_namespace/namespace.test/", 
mapper.cleanupActionName(namespace));
     }
 
+    public void testRejectedNamesDoNotInjectLineBreaksIntoLog() {
+        DefaultActionMapper mapper = new DefaultActionMapper();
+
+        try (LogCapture logs = new LogCapture(DefaultActionMapper.class)) {
+            mapper.cleanupNamespaceName("/ns\n12:00:00 ERROR forged");
+            mapper.cleanupActionName("action\r\n12:00:00 ERROR forged");
+            mapper.cleanupMethodName("method\n12:00:00 ERROR forged");
+
+            assertEquals(3, logs.messages().size());
+            for (String message : logs.messages()) {
+                assertTrue(message, message.contains("12:00:00 ERROR forged"));
+                assertFalse(message, message.contains("\n") || 
message.contains("\r"));
+            }
+        }
+    }
+
     public void testAllowedActionNames() {
         DefaultActionMapper mapper = new DefaultActionMapper();
 
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapperTest.java
 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapperTest.java
index 79efccfa0..aa1652f24 100644
--- 
a/core/src/test/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapperTest.java
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapperTest.java
@@ -25,6 +25,7 @@ import java.util.Map;
 import junit.framework.TestCase;
 
 import org.apache.struts2.StrutsInternalTestCase;
+import org.apache.struts2.test.LogCapture;
 import org.apache.struts2.views.jsp.StrutsMockHttpServletRequest;
 
 /**
@@ -121,6 +122,17 @@ public class RestfulActionMapperTest extends 
StrutsInternalTestCase {
         assertEquals("my-app.action", am.getName());
     }
 
+    public void testRejectedActionNameDoesNotInjectLineBreaksIntoLog() {
+        try (LogCapture logs = new LogCapture(RestfulActionMapper.class)) {
+            mapper.cleanupActionName("action\r\n12:00:00 ERROR forged");
+
+            assertEquals(1, logs.messages().size());
+            String message = logs.messages().get(0);
+            assertTrue(message, message.contains("12:00:00 ERROR forged"));
+            assertFalse(message, message.contains("\n") || 
message.contains("\r"));
+        }
+    }
+
     protected void setUp() throws Exception {
         super.setUp();
         mapper = new RestfulActionMapper();
diff --git 
a/core/src/test/java/org/apache/struts2/interceptor/parameter/ParametersInterceptorTest.java
 
b/core/src/test/java/org/apache/struts2/interceptor/parameter/ParametersInterceptorTest.java
index de17ba7cb..9b0d52b6b 100644
--- 
a/core/src/test/java/org/apache/struts2/interceptor/parameter/ParametersInterceptorTest.java
+++ 
b/core/src/test/java/org/apache/struts2/interceptor/parameter/ParametersInterceptorTest.java
@@ -44,6 +44,7 @@ import org.apache.struts2.ognl.accessor.CompoundRootAccessor;
 import org.apache.struts2.ognl.accessor.RootAccessor;
 import org.apache.struts2.util.ValueStack;
 import org.apache.struts2.util.ValueStackFactory;
+import org.apache.struts2.test.LogCapture;
 import org.apache.struts2.util.reflection.ReflectionContextState;
 import ognl.OgnlContext;
 import org.apache.struts2.action.NoParameters;
@@ -1009,6 +1010,27 @@ public class ParametersInterceptorTest extends 
XWorkTestCase {
         return actual;
     }
 
+    public void 
testRejectedParameterNamesAndValuesDoNotInjectLineBreaksIntoLog() {
+        for (String devMode : new String[]{"false", "true"}) {
+            ParametersInterceptor pi = createParametersInterceptor();
+            pi.setDevMode(devMode);
+            pi.setParamNameMaxLength(10);
+            pi.setAcceptedValuePatterns("fooValue");
+            pi.setExcludedValuePatterns("barValue");
+
+            try (LogCapture logs = new 
LogCapture(ParametersInterceptor.class)) {
+                pi.isAcceptableName("tooLongName\n12:00:00 ERROR forged");
+                pi.isAcceptableName("a\n12:00:00");
+                pi.isAcceptableValue("name", "bazValue\n12:00:00 ERROR 
forged");
+
+                assertFalse(logs.messages().isEmpty());
+                for (String message : logs.messages()) {
+                    assertFalse("devMode=" + devMode + ": " + message, 
message.contains("\n12:00:00"));
+                }
+            }
+        }
+    }
+
     private ParametersInterceptor createParametersInterceptor() {
         ParametersInterceptor pi = new ParametersInterceptor();
         container.inject(pi);
diff --git 
a/core/src/test/java/org/apache/struts2/locale/DefaultLocaleProviderTest.java 
b/core/src/test/java/org/apache/struts2/locale/DefaultLocaleProviderTest.java
index b58211fec..958a73274 100644
--- 
a/core/src/test/java/org/apache/struts2/locale/DefaultLocaleProviderTest.java
+++ 
b/core/src/test/java/org/apache/struts2/locale/DefaultLocaleProviderTest.java
@@ -19,6 +19,7 @@
 package org.apache.struts2.locale;
 
 import org.apache.struts2.ActionContext;
+import org.apache.struts2.test.LogCapture;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
@@ -50,6 +51,21 @@ public class DefaultLocaleProviderTest {
         ActionContext.clear();
     }
 
+    @Test
+    public void toLocaleRejectedValueDoesNotInjectLineBreaksIntoLog() {
+        try (LogCapture logs = new LogCapture(DefaultLocaleProvider.class)) {
+            // when
+            Locale actual = provider.toLocale("en\n12:00:00 ERROR forged");
+
+            // then
+            assertNull(actual);
+            assertEquals(1, logs.messages().size());
+            String message = logs.messages().get(0);
+            assertTrue(message, message.contains("12:00:00 ERROR forged"));
+            assertFalse(message, message.contains("\n") || 
message.contains("\r"));
+        }
+    }
+
     @Test
     public void getLocale() {
         // given
diff --git a/core/src/test/java/org/apache/struts2/test/LogCapture.java 
b/core/src/test/java/org/apache/struts2/test/LogCapture.java
new file mode 100644
index 000000000..58d021594
--- /dev/null
+++ b/core/src/test/java/org/apache/struts2/test/LogCapture.java
@@ -0,0 +1,62 @@
+/*
+ * 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.struts2.test;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Property;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Collects what a class logs while open, as the formatted message followed by 
the exception, if any. Only levels
+ * enabled by the test log configuration are seen.
+ */
+public class LogCapture implements AutoCloseable {
+
+    private final Logger logger;
+    private final AbstractAppender appender;
+    private final List<String> messages = new CopyOnWriteArrayList<>();
+
+    public LogCapture(Class<?> source) {
+        logger = (Logger) LogManager.getLogger(source);
+        appender = new AbstractAppender("LogCapture-" + System.nanoTime(), 
null, null, false, Property.EMPTY_ARRAY) {
+            @Override
+            public void append(LogEvent event) {
+                String message = event.getMessage().getFormattedMessage();
+                messages.add(event.getThrown() == null ? message : message + " 
" + event.getThrown());
+            }
+        };
+        appender.start();
+        logger.addAppender(appender);
+    }
+
+    public List<String> messages() {
+        return messages;
+    }
+
+    @Override
+    public void close() {
+        logger.removeAppender(appender);
+        appender.stop();
+    }
+}
diff --git a/core/src/test/java/org/apache/struts2/util/TokenHelperTest.java 
b/core/src/test/java/org/apache/struts2/util/TokenHelperTest.java
index 07add2a07..85fcf3dcc 100644
--- a/core/src/test/java/org/apache/struts2/util/TokenHelperTest.java
+++ b/core/src/test/java/org/apache/struts2/util/TokenHelperTest.java
@@ -18,9 +18,14 @@
  */
 package org.apache.struts2.util;
 
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.config.Configurator;
 import org.apache.struts2.ActionContext;
 import junit.framework.TestCase;
 import org.apache.struts2.dispatcher.HttpParameters;
+import org.apache.struts2.test.LogCapture;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -61,6 +66,40 @@ public class TokenHelperTest extends TestCase {
         assertEquals(token, session.get(sessionTokenName));
     }
 
+    public void testMissingTokenNameDoesNotInjectLineBreaksIntoLog() {
+        ActionContext.getContext().withParameters(HttpParameters.create(new 
HashMap<String, String[]>()).build());
+
+        try (LogCapture logs = new LogCapture(TokenHelper.class)) {
+            assertNull(TokenHelper.getToken("token\n12:00:00 ERROR forged"));
+
+            assertEquals(1, logs.messages().size());
+            String message = logs.messages().get(0);
+            assertTrue(message, message.contains("12:00:00 ERROR forged"));
+            assertFalse(message, message.contains("\n") || 
message.contains("\r"));
+        }
+    }
+
+    public void 
testMissingTokenForTokenNameDoesNotInjectLineBreaksIntoDebugLog() {
+        Map<String, String[]> params = new HashMap<>();
+        params.put(TokenHelper.TOKEN_NAME_FIELD, new String[]{"token\n12:00:00 
ERROR forged"});
+        
ActionContext.getContext().withParameters(HttpParameters.create(params).build());
+
+        Logger logger = (Logger) LogManager.getLogger(TokenHelper.class);
+        Level previousLevel = logger.getLevel();
+        Configurator.setLevel(logger.getName(), Level.DEBUG);
+        try (LogCapture logs = new LogCapture(TokenHelper.class)) {
+            assertFalse(TokenHelper.validToken());
+
+            assertFalse(logs.messages().isEmpty());
+            for (String message : logs.messages()) {
+                assertFalse(message, message.contains("\n") || 
message.contains("\r"));
+            }
+            assertTrue(logs.messages().stream().anyMatch(m -> m.contains("No 
token found for token name")));
+        } finally {
+            Configurator.setLevel(logger.getName(), previousLevel);
+        }
+    }
+
     public void testValidToken() {
         String tokenName = "validTokenTest";
         String token = TokenHelper.setToken(tokenName);
diff --git 
a/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java 
b/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java
index 9386a618d..a2002c9ab 100644
--- a/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java
+++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java
@@ -51,6 +51,8 @@ import java.lang.reflect.Type;
 import java.util.*;
 import java.util.regex.Pattern;
 
+import static org.apache.commons.lang3.StringUtils.normalizeSpace;
+
 /**
  * Populates an action from a JSON string
  */
@@ -297,20 +299,20 @@ public class JSONInterceptor extends AbstractInterceptor {
      */
     private boolean isAcceptableNode(String fullPath, Object target, Object 
action) {
         if (fullPath.length() > paramNameMaxLength) {
-            LOG.warn("JSON body parameter [{}] is too long, allowed length is 
[{}]; rejected", fullPath, paramNameMaxLength);
+            LOG.warn("JSON body parameter [{}] is too long, allowed length is 
[{}]; rejected", normalizeSpace(fullPath), paramNameMaxLength);
             return false;
         }
         if (excludedPatterns != null && 
excludedPatterns.isExcluded(fullPath).isExcluded()) {
-            LOG.warn("JSON body parameter [{}] matches an excluded pattern; 
rejected", fullPath);
+            LOG.warn("JSON body parameter [{}] matches an excluded pattern; 
rejected", normalizeSpace(fullPath));
             return false;
         }
         if (!parameterAuthorizer.isAuthorized(fullPath, target, action)) {
             LOG.warn("JSON body parameter [{}] rejected by @StrutsParameter 
authorization on [{}]",
-                    fullPath, target.getClass().getName());
+                    normalizeSpace(fullPath), target.getClass().getName());
             return false;
         }
         if (applyPropertyFiltersToInput && 
!isAcceptedByPropertyFilters(fullPath)) {
-            LOG.debug("JSON body parameter [{}] rejected by 
excludeProperties/includeProperties on input", fullPath);
+            LOG.debug("JSON body parameter [{}] rejected by 
excludeProperties/includeProperties on input", normalizeSpace(fullPath));
             return false;
         }
         return true;
@@ -326,11 +328,11 @@ public class JSONInterceptor extends AbstractInterceptor {
      */
     private boolean isAcceptableLeafName(String fullPath, Object action) {
         if (acceptedPatterns != null && 
!acceptedPatterns.isAccepted(fullPath).isAccepted()) {
-            LOG.warn("JSON body parameter [{}] does not match any accepted 
pattern; rejected", fullPath);
+            LOG.warn("JSON body parameter [{}] does not match any accepted 
pattern; rejected", normalizeSpace(fullPath));
             return false;
         }
         if (action instanceof ParameterNameAware nameAware && 
!nameAware.acceptableParameterName(fullPath)) {
-            LOG.debug("JSON body parameter [{}] rejected by ParameterNameAware 
action", fullPath);
+            LOG.debug("JSON body parameter [{}] rejected by ParameterNameAware 
action", normalizeSpace(fullPath));
             return false;
         }
         return true;
@@ -358,18 +360,18 @@ public class JSONInterceptor extends AbstractInterceptor {
     private boolean isAcceptableValue(String fullPath, Object value, Object 
action) {
         String stringValue = value == null ? null : String.valueOf(value);
         if (action instanceof ParameterValueAware valueAware && 
!valueAware.acceptableParameterValue(stringValue)) {
-            LOG.debug("JSON body value for parameter [{}] rejected by 
ParameterValueAware action", fullPath);
+            LOG.debug("JSON body value for parameter [{}] rejected by 
ParameterValueAware action", normalizeSpace(fullPath));
             return false;
         }
         if (stringValue == null || stringValue.isEmpty()) {
             return true;
         }
         if (isValueExcluded(stringValue)) {
-            LOG.warn("JSON body value [{}] for parameter [{}] matches an 
excluded value pattern; rejected", stringValue, fullPath);
+            LOG.warn("JSON body value [{}] for parameter [{}] matches an 
excluded value pattern; rejected", normalizeSpace(stringValue), 
normalizeSpace(fullPath));
             return false;
         }
         if (!isValueAccepted(stringValue)) {
-            LOG.warn("JSON body value [{}] for parameter [{}] does not match 
any accepted value pattern; rejected", stringValue, fullPath);
+            LOG.warn("JSON body value [{}] for parameter [{}] does not match 
any accepted value pattern; rejected", normalizeSpace(stringValue), 
normalizeSpace(fullPath));
             return false;
         }
         return true;
diff --git 
a/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java 
b/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java
index 32853534a..e7508fee4 100644
--- 
a/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java
+++ 
b/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java
@@ -18,6 +18,13 @@
  */
 package org.apache.struts2.json;
 
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Configurator;
+import org.apache.logging.log4j.core.config.Property;
 import org.apache.struts2.ActionContext;
 import org.apache.struts2.mock.MockActionInvocation;
 import org.apache.struts2.util.ValueStack;
@@ -31,6 +38,7 @@ import org.springframework.mock.web.MockServletContext;
 import java.util.Calendar;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 public class JSONInterceptorTest extends StrutsTestCase {
     private MockActionInvocationEx invocation;
@@ -714,6 +722,47 @@ public class JSONInterceptorTest extends StrutsTestCase {
         assertNull(action.getBar());
     }
 
+    public void testRejectedKeysAndValuesDoNotInjectLineBreaksIntoLog() throws 
Exception {
+        this.request.setContent("{\"foo\":\"a\\n12:00:00 ERROR forged\", 
\"b\\n12:00:00 ERROR forged\":\"b\"}".getBytes());
+        this.request.addHeader("Content-Type", "application/json");
+
+        JSONInterceptor interceptor = createInterceptor();
+        org.apache.struts2.security.DefaultAcceptedPatternsChecker accepted =
+                new 
org.apache.struts2.security.DefaultAcceptedPatternsChecker();
+        interceptor.setAcceptedPatterns(accepted);
+        interceptor.setAcceptedValuePatterns("allowed");
+        TestAction action = new TestAction();
+
+        this.invocation.setAction(action);
+        this.invocation.getStack().push(action);
+
+        List<String> messages = new CopyOnWriteArrayList<>();
+        Logger logger = (Logger) LogManager.getLogger(JSONInterceptor.class);
+        AbstractAppender appender = new 
AbstractAppender("JSONInterceptorTest", null, null, false, 
Property.EMPTY_ARRAY) {
+            @Override
+            public void append(LogEvent event) {
+                messages.add(event.getMessage().getFormattedMessage());
+            }
+        };
+        appender.start();
+        logger.addAppender(appender);
+        Level previousLevel = logger.getLevel();
+        Configurator.setLevel(logger.getName(), Level.WARN);
+        try {
+            interceptor.intercept(this.invocation);
+        } finally {
+            Configurator.setLevel(logger.getName(), previousLevel);
+            logger.removeAppender(appender);
+            appender.stop();
+        }
+
+        assertFalse(messages.isEmpty());
+        for (String message : messages) {
+            assertFalse(message, message.contains("\n") || 
message.contains("\r"));
+        }
+        assertTrue(messages.stream().anyMatch(m -> m.contains("12:00:00 ERROR 
forged")));
+    }
+
     public void testAcceptedNamePatternRejectsKey() throws Exception {
         this.request.setContent("{\"foo\":\"a\", \"bar\":\"b\"}".getBytes());
         this.request.addHeader("Content-Type", "application/json");

Reply via email to