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.
*/