From e7f34c2e73eb5e6fb2ae19cc04cc779f47578942 Mon Sep 17 00:00:00 2001 From: Zibran Khan Date: Sat, 10 Oct 2026 01:32:04 +0530 Subject: [PATCH] exclude synthetic and ignored fields from json field filtering --- .../JSONStreamingMessageFormatter.java | 8 +++- .../MoshiStreamingMessageFormatter.java | 17 ++++++- .../FieldFilteringMessageFormatterTest.java | 34 ++++++++++++++ .../MoshiStreamingMessageFormatterTest.java | 47 +++++++++++++++++++ 4 files changed, 103 insertions(+), 3 deletions(-) diff --git a/modules/json/src/org/apache/axis2/json/streaming/JSONStreamingMessageFormatter.java b/modules/json/src/org/apache/axis2/json/streaming/JSONStreamingMessageFormatter.java index 2214a0ce45..4e37684950 100644 --- a/modules/json/src/org/apache/axis2/json/streaming/JSONStreamingMessageFormatter.java +++ b/modules/json/src/org/apache/axis2/json/streaming/JSONStreamingMessageFormatter.java @@ -455,13 +455,19 @@ private void writeFilteredPojoGson(JsonWriter jsonWriter, Object pojo, /** * Collect all non-static, non-transient fields from the class hierarchy. + * + *

Synthetic fields (such as the {@code this$0} reference to an + * enclosing instance) are excluded, matching GSON's own excluder. The + * field filter selects a subset of the normal response; it must not make + * a field reachable that the unfiltered output never contains.

*/ private static List getAllFields(Class clazz) { List result = new ArrayList<>(); for (Class c = clazz; c != null && c != Object.class; c = c.getSuperclass()) { for (Field field : c.getDeclaredFields()) { int mods = field.getModifiers(); - if (!Modifier.isStatic(mods) && !Modifier.isTransient(mods)) { + if (!Modifier.isStatic(mods) && !Modifier.isTransient(mods) + && !field.isSynthetic()) { result.add(field); } } diff --git a/modules/json/src/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatter.java b/modules/json/src/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatter.java index b966b832f5..2319d3d616 100644 --- a/modules/json/src/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatter.java +++ b/modules/json/src/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatter.java @@ -19,6 +19,7 @@ package org.apache.axis2.json.streaming; +import com.squareup.moshi.Json; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; @@ -634,6 +635,12 @@ private void writeFilteredPojo(JsonWriter jsonWriter, Object pojo, * including) Object. This ensures inherited fields are included when * a response object extends a base class. * + *

Fields that Moshi itself never serializes are excluded as well: + * synthetic fields (such as the {@code this$0} reference to an enclosing + * instance) and fields annotated {@code @Json(ignore = true)}. The field + * filter selects a subset of the normal response; it must not make a + * field reachable that the unfiltered output never contains.

+ * *

Note: this method reflects over the class on each call. For extreme * performance needs, the result could be cached in a * {@code ConcurrentHashMap, List>}. The current approach @@ -644,9 +651,15 @@ private static List getAllFields(Class clazz) { for (Class c = clazz; c != null && c != Object.class; c = c.getSuperclass()) { for (Field field : c.getDeclaredFields()) { int mods = field.getModifiers(); - if (!Modifier.isStatic(mods) && !Modifier.isTransient(mods)) { - result.add(field); + if (Modifier.isStatic(mods) || Modifier.isTransient(mods) + || field.isSynthetic()) { + continue; + } + Json json = field.getAnnotation(Json.class); + if (json != null && json.ignore()) { + continue; } + result.add(field); } } return result; diff --git a/modules/json/test/org/apache/axis2/json/streaming/FieldFilteringMessageFormatterTest.java b/modules/json/test/org/apache/axis2/json/streaming/FieldFilteringMessageFormatterTest.java index 18a8f6916b..7a215ce51e 100644 --- a/modules/json/test/org/apache/axis2/json/streaming/FieldFilteringMessageFormatterTest.java +++ b/modules/json/test/org/apache/axis2/json/streaming/FieldFilteringMessageFormatterTest.java @@ -271,6 +271,28 @@ public void testStaticAndTransientFieldsNeverIncluded() throws Exception { response.getAsJsonObject().has("tempCache")); } + @Test + public void testSyntheticFieldsNeverIncluded() throws Exception { + // this$0 is a synthetic field pointing at the enclosing instance. + // Neither GSON nor Moshi serializes it on the unfiltered path, so + // requesting it by name must not expose the enclosing object's state. + EnclosingService svc = new EnclosingService("db-password-123"); + setReturnObject(svc.new Result("ok")); + outMsgContext.setProperty(JsonConstant.FIELD_FILTER, + setOf("status", "this$0")); + + formatter.writeTo(outMsgContext, outputFormat, outputStream, false); + String raw = outputStream.toString("UTF-8"); + JsonElement response = parseResponse(); + + Assert.assertEquals("ok", + response.getAsJsonObject().get("status").getAsString()); + Assert.assertFalse("synthetic this$0 must never appear", + response.getAsJsonObject().has("this$0")); + Assert.assertFalse("enclosing instance state must not leak", + raw.contains("db-password-123")); + } + // ── parseFieldsCsv / parseFieldsFromUrl tests ───────────────────────── @Test @@ -436,6 +458,18 @@ public ChildData(String s, double v, String e) { } } + /** Enclosing object whose state must not leak through a synthetic this$0. */ + public static class EnclosingService { + public String dbPassword; + public EnclosingService(String p) { dbPassword = p; } + + /** Non-static inner class — carries a synthetic this$0 field. */ + public class Result { + public String status; + public Result(String s) { status = s; } + } + } + /** POJO with static and transient fields for modifier filtering test. */ public static class StaticTransientData { public static final String CONSTANT = "should-never-serialize"; diff --git a/modules/json/test/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatterTest.java b/modules/json/test/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatterTest.java index ac1c7576f5..e66f342104 100644 --- a/modules/json/test/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatterTest.java +++ b/modules/json/test/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatterTest.java @@ -19,6 +19,7 @@ package org.apache.axis2.json.streaming; +import com.squareup.moshi.Json; import com.squareup.moshi.Moshi; import org.apache.axiom.om.OMAbstractFactory; import org.apache.axiom.om.OMElement; @@ -36,6 +37,8 @@ import org.junit.Test; import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.LinkedHashSet; /** * Unit tests for {@link MoshiStreamingMessageFormatter}. @@ -159,6 +162,36 @@ public void testOutputMatchesStandardFormatter() throws Exception { standardResult, streamingResult); } + /** + * Test that a field excluded from serialization with + * {@code @Json(ignore = true)} cannot be pulled into the response by + * naming it in the field filter. The filter may only narrow the + * normal Moshi output, never widen it. + */ + @Test + public void testFieldFilterDoesNotExposeIgnoredField() throws Exception { + CredentialData data = new CredentialData("public-label", "secret-token-xyz"); + outMsgContext.setProperty(JsonConstant.RETURN_OBJECT, data); + outMsgContext.setProperty(JsonConstant.RETURN_TYPE, CredentialData.class); + + // Unfiltered baseline: Moshi never serializes the ignored field. + MoshiStreamingMessageFormatter formatter = new MoshiStreamingMessageFormatter(); + formatter.writeTo(outMsgContext, outputFormat, outputStream, false); + Assert.assertFalse("ignored field must be absent from the unfiltered response", + outputStream.toString("UTF-8").contains("secret-token-xyz")); + + // Filtered: requesting the ignored field by name must not expose it. + outputStream.reset(); + outMsgContext.setProperty(JsonConstant.FIELD_FILTER, + new LinkedHashSet<>(Arrays.asList("label", "apiToken"))); + formatter.writeTo(outMsgContext, outputFormat, outputStream, false); + + String result = outputStream.toString("UTF-8"); + Assert.assertTrue(result.contains("\"label\":\"public-label\"")); + Assert.assertFalse("@Json(ignore = true) field must not be exposed by the field filter", + result.contains("secret-token-xyz")); + } + /** * Test content type passthrough. */ @@ -186,6 +219,20 @@ private OMElement createFaultOMElement() { return fault; } + /** POJO with a field opted out of serialization via {@code @Json(ignore = true)}. */ + public static class CredentialData { + public String label; + @Json(ignore = true) + public String apiToken; + + public CredentialData() {} + + public CredentialData(String label, String apiToken) { + this.label = label; + this.apiToken = apiToken; + } + } + /** * Simple POJO for test serialization. */