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();
