11package com .datadog .appsec .event .data ;
22
33import datadog .trace .api .Platform ;
4- import java .lang .reflect .*;
4+ import java .lang .reflect .Array ;
5+ import java .lang .reflect .Field ;
6+ import java .lang .reflect .InvocationTargetException ;
7+ import java .lang .reflect .Method ;
8+ import java .lang .reflect .Modifier ;
59import java .util .ArrayList ;
610import java .util .HashMap ;
711import java .util .List ;
@@ -32,38 +36,89 @@ public final class ObjectIntrospection {
3236 private ObjectIntrospection () {}
3337
3438 /**
35- * Converts arbitrary objects to strings, maps and lists, by using reflection. This serves two
36- * main purposes: - the objects can be inspected by the appsec subsystem and passed to the WAF. -
37- * By creating new containers and not transforming only immutable objects like strings, the new
38- * object can be safely manipulated by the appsec subsystem without worrying about modifications
39- * in other threads.
39+ * Converts arbitrary objects compatible with ddwaf_object. Possible types in the result are:
40+ *
41+ * <ul>
42+ * <li>Null
43+ * <li>Strings
44+ * <li>Boolean
45+ * <li>Byte, Short, Integer, Long (will be serialized as int64)
46+ * <li>Float, Double (will be serialized as float64)
47+ * <li>Maps with string keys
48+ * <li>Lists
49+ * </ul>
50+ *
51+ * This serves two purposes:
52+ *
53+ * <ul>
54+ * <li>The objects can be inspected by the appsec subsystem and passed to the WAF.
55+ * <li>>By creating new containers and not transforming only immutable objects like strings, the
56+ * new object can be safely manipulated by the appsec subsystem without worrying about
57+ * modifications in other threads.
58+ * </ul>
4059 *
4160 * <p>Certain instance fields are excluded. Right now, this includes metaClass fields in Groovy
4261 * objects and this$0 fields in inner classes.
4362 *
63+ * <p>Only string values are preserved. Numbers or booleans are removed, since we do not expect
64+ * rules to detect malicious payloads in these types. An exception to this are map keys, which are
65+ * always converted to strings.
66+ *
4467 * @param obj an arbitrary object
4568 * @return the converted object
4669 */
4770 public static Object convert (Object obj ) {
48- return guardedConversion (obj , 0 , new int [] { MAX_ELEMENTS } );
71+ return guardedConversion (obj , 0 , new State () );
4972 }
5073
51- private static Object guardedConversion (Object obj , int depth , int [] elemsLeft ) {
74+ private static class State {
75+ int elemsLeft = MAX_ELEMENTS ;
76+ int invalidKeyId ;
77+ }
78+
79+ private static Object guardedConversion (Object obj , int depth , State state ) {
5280 try {
53- return doConversion (obj , depth , elemsLeft );
81+ return doConversion (obj , depth , state );
5482 } catch (Throwable t ) {
55- return "<error: " + t .getMessage () + ">" ;
83+ // TODO: Use invalid object
84+ return "error:" + t .getMessage ();
85+ }
86+ }
87+
88+ private static String keyConversion (Object key , State state ) {
89+ state .elemsLeft --;
90+ if (state .elemsLeft <= 0 ) {
91+ return null ;
92+ }
93+ if (key == null ) {
94+ return "null" ;
95+ }
96+ if (key instanceof String ) {
97+ return (String ) key ;
5698 }
99+ if (key instanceof Number
100+ || key instanceof Boolean
101+ || key instanceof Character
102+ || key instanceof CharSequence ) {
103+ return key .toString ();
104+ }
105+ return "invalid_key:" + (++state .invalidKeyId );
57106 }
58107
59- private static Object doConversion (Object obj , int depth , int [] elemsLeft ) {
60- elemsLeft [ 0 ] --;
61- if (elemsLeft [ 0 ] <= 0 || obj == null || depth > MAX_DEPTH ) {
108+ private static Object doConversion (Object obj , int depth , State state ) {
109+ state . elemsLeft --;
110+ if (state . elemsLeft <= 0 || obj == null || depth > MAX_DEPTH ) {
62111 return null ;
63112 }
64113
65- // char sequences / numbers
66- if (obj instanceof CharSequence || obj instanceof Number ) {
114+ // strings, booleans and numbers are preserved
115+ if (obj instanceof String || obj instanceof Boolean || obj instanceof Number ) {
116+ return obj ;
117+ }
118+
119+ // char sequences are transformed just in case they are not immutable,
120+ // single char sequences are transformed to strings for ddwaf compatibility.
121+ if (obj instanceof CharSequence || obj instanceof Character ) {
67122 return obj .toString ();
68123 }
69124
@@ -72,12 +127,12 @@ private static Object doConversion(Object obj, int depth, int[] elemsLeft) {
72127 Map <Object , Object > newMap = new HashMap <>((int ) Math .ceil (((Map ) obj ).size () / .75 ));
73128 for (Map .Entry <?, ?> e : ((Map <?, ?>) obj ).entrySet ()) {
74129 Object key = e .getKey ();
75- Object newKey = guardedConversion (e .getKey (), depth + 1 , elemsLeft );
130+ Object newKey = keyConversion (e .getKey (), state );
76131 if (newKey == null && key != null ) {
77132 // probably we're out of elements anyway
78133 continue ;
79134 }
80- newMap .put (newKey , guardedConversion (e .getValue (), depth + 1 , elemsLeft ));
135+ newMap .put (newKey , guardedConversion (e .getValue (), depth + 1 , state ));
81136 }
82137 return newMap ;
83138 }
@@ -91,10 +146,10 @@ private static Object doConversion(Object obj, int depth, int[] elemsLeft) {
91146 newList = new ArrayList <>();
92147 }
93148 for (Object o : ((Iterable <?>) obj )) {
94- if (elemsLeft [ 0 ] <= 0 ) {
149+ if (state . elemsLeft <= 0 ) {
95150 break ;
96151 }
97- newList .add (guardedConversion (o , depth + 1 , elemsLeft ));
152+ newList .add (guardedConversion (o , depth + 1 , state ));
98153 }
99154 return newList ;
100155 }
@@ -104,8 +159,8 @@ private static Object doConversion(Object obj, int depth, int[] elemsLeft) {
104159 if (clazz .isArray ()) {
105160 int length = Array .getLength (obj );
106161 List <Object > newList = new ArrayList <>(length );
107- for (int i = 0 ; i < length && elemsLeft [ 0 ] > 0 ; i ++) {
108- newList .add (guardedConversion (Array .get (obj , i ), depth + 1 , elemsLeft ));
162+ for (int i = 0 ; i < length && state . elemsLeft > 0 ; i ++) {
163+ newList .add (guardedConversion (Array .get (obj , i ), depth + 1 , state ));
109164 }
110165 return newList ;
111166 }
@@ -122,7 +177,7 @@ private static Object doConversion(Object obj, int depth, int[] elemsLeft) {
122177 outer :
123178 for (Field [] fields : allFields ) {
124179 for (Field f : fields ) {
125- if (elemsLeft [ 0 ] <= 0 ) {
180+ if (state . elemsLeft <= 0 ) {
126181 break outer ;
127182 }
128183 if (Modifier .isStatic (f .getModifiers ())) {
@@ -132,19 +187,21 @@ private static Object doConversion(Object obj, int depth, int[] elemsLeft) {
132187 continue ;
133188 }
134189 String name = f .getName ();
135- if (name . equals ( "this$0" )) {
190+ if (ignoredFieldName ( name )) {
136191 continue ;
137192 }
138193
139194 if (setAccessible (f )) {
140195 try {
141- newMap .put (f .getName (), guardedConversion (f .get (obj ), depth + 1 , elemsLeft ));
196+ newMap .put (f .getName (), guardedConversion (f .get (obj ), depth + 1 , state ));
142197 } catch (IllegalAccessException e ) {
143198 log .error ("Unable to get field value" , e );
199+ // TODO: Use invalid object
144200 }
145201 } else {
146202 // One of fields is inaccessible, might be it's Strongly Encapsulated Internal class
147203 // consider it as integral object without introspection
204+ // TODO: Use invalid object
148205 return obj .toString ();
149206 }
150207 }
@@ -153,6 +210,17 @@ private static Object doConversion(Object obj, int depth, int[] elemsLeft) {
153210 return newMap ;
154211 }
155212
213+ private static boolean ignoredFieldName (final String name ) {
214+ switch (name ) {
215+ case "this$0" :
216+ case "memoizedHashCode" :
217+ case "memoizedSize" :
218+ return true ;
219+ default :
220+ return false ;
221+ }
222+ }
223+
156224 /**
157225 * Try to make field accessible
158226 *
0 commit comments