diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index 3ded23f61..1c329e174 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -3472,12 +3472,15 @@ private Type[] getMapTypes(Type type) { * of the given class. It supports basic data types including int, double, float, * long, and boolean (as well as their boxed counterparts). The class must have a * no-argument constructor, and the field names in the class must match the keys - * in the JSON string. + * in the JSON string. Static fields are ignored; see {@link #fromJson(Class)} for + * how the remaining fields are bound. * * @param the type of the object to return * @param jsonString json in string format * @param clazz the class of the object to be returned * @return an instance of Object T with fields populated from the JSON string + * @throws JSONException if jsonString is not valid JSON, or if it cannot be bound to clazz + * as described in {@link #fromJson(Class)} */ public static T fromJson(String jsonString, Class clazz) { JSONObject jsonObject = new JSONObject(jsonString); @@ -3493,15 +3496,20 @@ public static T fromJson(String jsonString, Class clazz) { * The target class must have a no-argument constructor, and its field names must match * the keys in the JSON string. Static fields are ignored. * - *

Note: Only classes that are explicitly supported and registered within - * the {@code JSONObject} context can be deserialized. If the provided class is not among those, - * this method will not be able to deserialize it. This ensures that only a limited and - * controlled set of types can be instantiated from JSON for safety and predictability. + *

Note: binding is driven by the keys present in the JSON, not by + * how the fields are declared. Every non-static field declared by the class is written + * when its name matches a key, including {@code private} and {@code transient} fields, + * and field access is widened with + * {@link java.lang.reflect.Field#setAccessible(boolean)}. {@link JSONPropertyIgnore} + * only suppresses the bean to JSONObject direction, so it does not keep a field from + * being populated here. JSON coming from an untrusted source should therefore be + * validated, or reduced to the expected keys, before it is handed to this method. * * @param clazz the class of the object to be returned * @param the type of the object * @return an instance of type {@code T} with fields populated from the JSON string - * @throws IllegalArgumentException if the class is not supported for deserialization + * @throws JSONException if the class has no accessible no-argument constructor, or if a + * field cannot be instantiated or set */ @SuppressWarnings("unchecked") public T fromJson(Class clazz) { diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java index a0f0638b0..cc6c7c782 100644 --- a/src/test/java/org/json/junit/JSONObjectTest.java +++ b/src/test/java/org/json/junit/JSONObjectTest.java @@ -67,6 +67,7 @@ import org.json.junit.data.CustomClassH; import org.json.junit.data.CustomClassI; import org.json.junit.data.CustomClassJ; +import org.json.junit.data.CustomClassK; import org.junit.After; import org.junit.Ignore; import org.junit.Test; @@ -3883,6 +3884,30 @@ public void jsonObjectParseFromJson_9() { } } + /** + * Pins down the field binding done by {@code fromJson(Class)}: every non-static + * declared field whose name matches a JSON key is written, no matter how the + * field is declared. Private fields, transient fields and fields hidden behind + * a {@code @JSONPropertyIgnore} getter are all populated, because that + * annotation only affects the bean to JSONObject direction. See issue #1076. + */ + @Test + public void jsonObjectParseFromJson_10() { + JSONObject object = new JSONObject() + .put("displayName", "bob") + .put("admin", true) + .put("tenantId", "victim-tenant") + .put("sessionKey", "attacker-supplied") + .put("superAdmin", true); + + CustomClassK bound = object.fromJson(CustomClassK.class); + + assertEquals("private field should be populated", true, bound.isAdmin()); + assertEquals("private field should be populated", "victim-tenant", bound.getTenantId()); + assertEquals("transient field should be populated", "attacker-supplied", bound.getSessionKey()); + assertEquals("field behind a @JSONPropertyIgnore getter should be populated", true, bound.isSuperAdmin()); + } + @Test public void testMaxNumberLength() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration(); diff --git a/src/test/java/org/json/junit/data/CustomClassK.java b/src/test/java/org/json/junit/data/CustomClassK.java new file mode 100644 index 000000000..7d64adb07 --- /dev/null +++ b/src/test/java/org/json/junit/data/CustomClassK.java @@ -0,0 +1,39 @@ +package org.json.junit.data; + +import org.json.JSONPropertyIgnore; + +/** + * Bean used to pin down how {@code JSONObject#fromJson(Class)} binds JSON keys + * to fields: {@code admin} and {@code tenantId} are private, {@code sessionKey} + * is transient, and {@code superAdmin} is hidden behind a getter annotated with + * {@code @JSONPropertyIgnore} (which only suppresses the bean to JSONObject + * direction). + */ +public class CustomClassK { + public String displayName; + private boolean admin; + private String tenantId; + private transient String sessionKey; + private boolean superAdmin; + + public CustomClassK() { + // Required for JSONObject#fromJson(Class) tests. + } + + public boolean isAdmin() { + return admin; + } + + public String getTenantId() { + return tenantId; + } + + public String getSessionKey() { + return sessionKey; + } + + @JSONPropertyIgnore + public boolean isSuperAdmin() { + return superAdmin; + } +}