Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -455,13 +455,19 @@ private void writeFilteredPojoGson(JsonWriter jsonWriter, Object pojo,

/**
* Collect all non-static, non-transient fields from the class hierarchy.
*
* <p>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.</p>
*/
private static List<Field> getAllFields(Class<?> clazz) {
List<Field> 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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
* <p>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.</p>
*
* <p>Note: this method reflects over the class on each call. For extreme
* performance needs, the result could be cached in a
* {@code ConcurrentHashMap<Class<?>, List<Field>>}. The current approach
Expand All @@ -644,9 +651,15 @@ private static List<Field> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}.
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down
Loading