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

lukaszlenart pushed a commit to branch WW-4858-json-parameter-filtering
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 2b39daf2f8be4016dd444137c00d784618d22988
Author: Lukasz Lenart <[email protected]>
AuthorDate: Wed Jul 8 20:43:56 2026 +0200

    WW-4858 feat(json): add opt-in excluded/accepted value patterns on JSON 
population
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../org/apache/struts2/json/JSONInterceptor.java   | 67 ++++++++++++++++++++++
 .../apache/struts2/json/JSONInterceptorTest.java   | 34 +++++++++++
 2 files changed, 101 insertions(+)

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 37fb2fec5..a3d919e9c 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
@@ -27,6 +27,7 @@ import org.apache.struts2.interceptor.AbstractInterceptor;
 import org.apache.struts2.interceptor.parameter.ParameterAuthorizer;
 import org.apache.struts2.security.AcceptedPatternsChecker;
 import org.apache.struts2.security.ExcludedPatternsChecker;
+import org.apache.struts2.util.TextParseUtil;
 import org.apache.struts2.util.ValueStack;
 import org.apache.struts2.util.WildcardUtil;
 import org.apache.commons.lang3.BooleanUtils;
@@ -86,6 +87,8 @@ public class JSONInterceptor extends AbstractInterceptor {
     private int maxStringLength = JSONReader.DEFAULT_MAX_STRING_LENGTH;
     private int maxKeyLength = JSONReader.DEFAULT_MAX_KEY_LENGTH;
     private int paramNameMaxLength = 100;
+    private Set<Pattern> excludedValuePatterns;
+    private Set<Pattern> acceptedValuePatterns;
 
     @SuppressWarnings("unchecked")
     public String intercept(ActionInvocation invocation) throws Exception {
@@ -297,9 +300,44 @@ public class JSONInterceptor extends AbstractInterceptor {
             LOG.debug("JSON body value for parameter [{}] rejected by 
ParameterValueAware action", 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);
+            return false;
+        }
+        if (!isValueAccepted(stringValue)) {
+            LOG.warn("JSON body value [{}] for parameter [{}] does not match 
any accepted value pattern; rejected", stringValue, fullPath);
+            return false;
+        }
         return true;
     }
 
+    private boolean isValueExcluded(String value) {
+        if (excludedValuePatterns == null || excludedValuePatterns.isEmpty()) {
+            return false;
+        }
+        for (Pattern pattern : excludedValuePatterns) {
+            if (pattern.matcher(value).matches()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isValueAccepted(String value) {
+        if (acceptedValuePatterns == null || acceptedValuePatterns.isEmpty()) {
+            return true;
+        }
+        for (Pattern pattern : acceptedValuePatterns) {
+            if (pattern.matcher(value).matches()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     protected String readContentType(HttpServletRequest request) {
         String contentType = request.getHeader("Content-Type");
         LOG.debug("Content Type from request: {}", contentType);
@@ -709,6 +747,35 @@ public class JSONInterceptor extends AbstractInterceptor {
         this.paramNameMaxLength = paramNameMaxLength;
     }
 
+    /**
+     * Sets a comma-delimited list of regular expressions to match JSON leaf 
values
+     * that should be removed. Opt-in: no patterns configured means no value 
filtering.
+     *
+     * @param commaDelim comma-delimited regular expressions
+     */
+    public void setExcludedValuePatterns(String commaDelim) {
+        this.excludedValuePatterns = compileValuePatterns(commaDelim);
+    }
+
+    /**
+     * Sets a comma-delimited list of regular expressions; when set, only JSON 
leaf values
+     * matching one of them are accepted. Opt-in: no patterns configured means 
no value filtering.
+     *
+     * @param commaDelim comma-delimited regular expressions
+     */
+    public void setAcceptedValuePatterns(String commaDelim) {
+        this.acceptedValuePatterns = compileValuePatterns(commaDelim);
+    }
+
+    private static Set<Pattern> compileValuePatterns(String commaDelim) {
+        Set<String> raw = TextParseUtil.commaDelimitedStringToSet(commaDelim);
+        Set<Pattern> compiled = new HashSet<>(raw.size());
+        for (String pattern : raw) {
+            compiled.add(Pattern.compile(pattern, Pattern.CASE_INSENSITIVE));
+        }
+        return Collections.unmodifiableSet(compiled);
+    }
+
     @Inject(value = JSONConstants.JSON_MAX_ELEMENTS, required = false)
     public void setMaxElements(String maxElements) {
         this.maxElements = Integer.parseInt(maxElements);
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 6287ae892..bfa0abf0c 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
@@ -797,6 +797,40 @@ public class JSONInterceptorTest extends StrutsTestCase {
         assertEquals("good", action.getBaz());
     }
 
+    public void testExcludedValuePatternRejectsValue() throws Exception {
+        this.request.setContent("{\"foo\":\"badvalue\", 
\"bar\":\"okvalue\"}".getBytes());
+        this.request.addHeader("Content-Type", "application/json");
+
+        JSONInterceptor interceptor = createInterceptor();
+        interceptor.setExcludedValuePatterns("badvalue");
+        TestAction action = new TestAction();
+
+        this.invocation.setAction(action);
+        this.invocation.getStack().push(action);
+
+        interceptor.intercept(this.invocation);
+
+        assertNull(action.getFoo());
+        assertEquals("okvalue", action.getBar());
+    }
+
+    public void testAcceptedValuePatternRejectsValue() throws Exception {
+        this.request.setContent("{\"foo\":\"allowed\", 
\"bar\":\"other\"}".getBytes());
+        this.request.addHeader("Content-Type", "application/json");
+
+        JSONInterceptor interceptor = createInterceptor();
+        interceptor.setAcceptedValuePatterns("allowed");
+        TestAction action = new TestAction();
+
+        this.invocation.setAction(action);
+        this.invocation.getStack().push(action);
+
+        interceptor.intercept(this.invocation);
+
+        assertEquals("allowed", action.getFoo());
+        assertNull(action.getBar());
+    }
+
     @Override
     protected void setUp() throws Exception {
         super.setUp();

Reply via email to