This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch WW-5626-cleanup in repository https://gitbox.apache.org/repos/asf/struts.git
commit 2a0bcc5f572b5ef6d7d894fbd6529ee5edaa3dd9 Author: Lukasz Lenart <[email protected]> AuthorDate: Mon May 4 12:50:26 2026 +0200 WW-5626 defensively skip non-String JSON keys in authorization filter The (String) cast in filterUnauthorizedKeysRecursive threw ClassCastException for any custom JSONReader producing non-String keys. Replace with an instanceof pattern that debug-logs and skips entries whose key cannot be converted to a parameter path. --- .../org/apache/struts2/json/JSONInterceptor.java | 9 +++++++- .../apache/struts2/json/JSONInterceptorTest.java | 26 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) 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 a0279f2f0..d0acfd4ce 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 @@ -215,7 +215,14 @@ public class JSONInterceptor extends AbstractInterceptor { Iterator<Map.Entry> it = json.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = it.next(); - String key = (String) entry.getKey(); + if (!(entry.getKey() instanceof String key)) { + // Defensive: a custom JSONReader could produce non-String keys. Skip — we cannot + // construct a parameter path for authorization, and JSONPopulator wouldn't bind + // these to bean properties anyway. + LOG.debug("Skipping JSON entry with non-String key [{}] of type [{}] under prefix [{}]", + entry.getKey(), entry.getKey() == null ? "null" : entry.getKey().getClass().getName(), prefix); + continue; + } String fullPath = prefix.isEmpty() ? key : prefix + "." + key; if (!parameterAuthorizer.isAuthorized(fullPath, target, action)) { 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 2203f6f29..ece2f1272 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 @@ -583,6 +583,32 @@ public class JSONInterceptorTest extends StrutsTestCase { assertNull(action.getBar()); } + public void testNonStringKeysAreSkippedByAuthorizationFilter() throws Exception { + // Simulate a custom JSON reader producing a Map with a non-String key. + // The authorizer should skip the entry rather than throw ClassCastException. + JSONInterceptor interceptor = new JSONInterceptor(); + JSONUtil jsonUtil = new JSONUtil(); + jsonUtil.setReader(new StrutsJSONReader()); + jsonUtil.setWriter(new StrutsJSONWriter()); + interceptor.setJsonUtil(jsonUtil); + interceptor.setParameterAuthorizer((parameterName, target, action) -> true); + + java.util.Map<Object, Object> mixedKeyMap = new java.util.LinkedHashMap<>(); + mixedKeyMap.put("validKey", "ok"); + mixedKeyMap.put(42, "shouldBeSkipped"); // Integer key, not String + + java.lang.reflect.Method method = JSONInterceptor.class.getDeclaredMethod( + "filterUnauthorizedKeys", java.util.Map.class, Object.class, Object.class); + method.setAccessible(true); + + // Should not throw ClassCastException + method.invoke(interceptor, mixedKeyMap, new TestAction(), new TestAction()); + + // The non-String key entry should still be present (skipped, not removed) + assertTrue("non-String-key entry should remain (skipped, not removed)", mixedKeyMap.containsKey(42)); + assertTrue("String-key entry should remain", mixedKeyMap.containsKey("validKey")); + } + public void testParameterAuthorizerAllowsAllWhenPermissive() throws Exception { // Same JSON body, but authorizer allows all this.request.setContent("{\"foo\":\"value1\", \"bar\":\"value2\"}".getBytes());
