Skip to content

Commit 901334b

Browse files
authored
Preserve original types before passing data to the WAF (#7220)
1 parent 174ea28 commit 901334b

File tree

3 files changed

+207
-52
lines changed

3 files changed

+207
-52
lines changed

dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/ObjectIntrospection.java

Lines changed: 92 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package com.datadog.appsec.event.data;
22

33
import 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;
59
import java.util.ArrayList;
610
import java.util.HashMap;
711
import 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
*

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/event/data/ObjectIntrospectionSpecification.groovy

Lines changed: 86 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,68 @@
11
package com.datadog.appsec.event.data
22

3-
43
import spock.lang.Specification
54

65
import java.nio.CharBuffer
76

87
import static com.datadog.appsec.event.data.ObjectIntrospection.convert
98

109
class ObjectIntrospectionSpecification extends Specification {
11-
void 'char sequences are converted to strings'() {
12-
setup:
13-
def charBuffer = CharBuffer.allocate(5)
14-
charBuffer.put('hello')
15-
charBuffer.position(0)
1610

11+
void 'null is preserved'() {
1712
expect:
18-
convert('hello') == 'hello'
19-
convert(charBuffer) == 'hello'
13+
convert(null) == null
2014
}
2115

22-
void 'numbers are converted to strings'() {
23-
expect:
24-
convert(5L) == '5'
25-
convert(0.33G) == '0.33'
16+
void 'type #type is preserved'() {
17+
when:
18+
def result = convert(input)
19+
20+
then:
21+
input.getClass() == type
22+
result.getClass() == type
23+
result == input
24+
25+
where:
26+
input | type
27+
'hello' | String
28+
true | Boolean
29+
(byte) 1 | Byte
30+
(short) 1 | Short
31+
1 | Integer
32+
1L | Long
33+
1.0F | Float
34+
(double) 1.0 | Double
35+
1G | BigInteger
36+
1.0G | BigDecimal
37+
}
38+
39+
void 'type #type is converted to string'() {
40+
when:
41+
def result = convert(input)
42+
43+
then:
44+
type.isAssignableFrom(input.getClass())
45+
result instanceof String
46+
result == output
47+
48+
where:
49+
input | type || output
50+
(char) 'a' | Character || 'a'
51+
createCharBuffer('hello') | CharBuffer || 'hello'
52+
}
53+
54+
static CharBuffer createCharBuffer(String s) {
55+
def charBuffer = CharBuffer.allocate(s.length())
56+
charBuffer.put(s)
57+
charBuffer.position(0)
58+
charBuffer
2659
}
2760

2861
void 'iterables are converted to lists'() {
2962
setup:
3063
def iter = new Iterable() {
31-
@Delegate Iterable delegate = ['a', 'b']
32-
}
64+
@Delegate Iterable delegate = ['a', 'b']
65+
}
3366

3467
expect:
3568
convert(iter) instanceof List
@@ -40,19 +73,25 @@ class ObjectIntrospectionSpecification extends Specification {
4073
void 'maps are converted to hash maps'() {
4174
setup:
4275
def map = new Map() {
43-
@Delegate Map map = [a: 'b']
44-
}
76+
@Delegate Map map = [a: 'b']
77+
}
4578

4679
expect:
4780
convert(map) instanceof HashMap
4881
convert(map) == [a: 'b']
4982
convert([(6): 'b']) == ['6': 'b']
83+
convert([(null): 'b']) == ['null': 'b']
84+
convert([(true): 'b']) == ['true': 'b']
85+
convert([('a' as Character): 'b']) == ['a': 'b']
86+
convert([(createCharBuffer('a')): 'b']) == ['a': 'b']
5087
}
5188

5289
void 'arrays are converted into lists'() {
5390
expect:
54-
convert([6, 'b'] as Object[]) == ['6', 'b']
55-
convert([1, 2] as int[]) == ['1', '2']
91+
convert([6, 'b'] as Object[]) == [6, 'b']
92+
convert([null, null] as Object[]) == [null, null]
93+
convert([1, 2] as int[]) == [1 as int, 2 as int]
94+
convert([1, 2] as byte[]) == [1 as byte, 2 as byte]
5695
}
5796

5897
@SuppressWarnings('UnusedPrivateField')
@@ -62,6 +101,7 @@ class ObjectIntrospectionSpecification extends Specification {
62101
private String a = 'b'
63102
private List l = [1, 2]
64103
}
104+
65105
class ClassToBeConvertedExt extends ClassToBeConverted {
66106
@SuppressWarnings('UnusedPrivateField')
67107
private String c = 'd'
@@ -70,7 +110,26 @@ class ObjectIntrospectionSpecification extends Specification {
70110
void 'other objects are converted into hash maps'() {
71111
expect:
72112
convert(new ClassToBeConverted()) instanceof HashMap
73-
convert(new ClassToBeConvertedExt()) == [c: 'd', a: 'b', l: ['1', '2']]
113+
convert(new ClassToBeConvertedExt()) == [c: 'd', a: 'b', l: [1, 2]]
114+
}
115+
116+
class ProtobufLikeClass {
117+
String c = 'd'
118+
int memoizedHashCode = 1
119+
int memoizedSize = 2
120+
}
121+
122+
void 'some field names are ignored'() {
123+
expect:
124+
convert(new ProtobufLikeClass()) instanceof HashMap
125+
convert(new ProtobufLikeClass()) == [c: 'd']
126+
}
127+
128+
void 'invalid keys are converted to special strings'() {
129+
expect:
130+
convert(Collections.singletonMap(new ClassToBeConverted(), 'a')) == ['invalid_key:1': 'a']
131+
convert([new ClassToBeConverted(): 'a', new ClassToBeConverted(): 'b']) == ['invalid_key:1': 'a', 'invalid_key:2': 'b']
132+
convert(Collections.singletonMap([1, 2], 'a')) == ['invalid_key:1': 'a']
74133
}
75134

76135
void 'max number of elements is honored'() {
@@ -81,7 +140,7 @@ class ObjectIntrospectionSpecification extends Specification {
81140
expect:
82141
convert([['a'] * 255])[0].size() == 254 // +2 for the lists
83142
convert([['a'] * 255 as String[]])[0].size() == 254 // +2 for the lists
84-
convert(m).size() == 127
143+
convert(m).size() == 127 // +1 for the map, 2 for each entry (key and value)
85144
}
86145

87146
void 'max depth is honored — array version'() {
@@ -98,7 +157,6 @@ class ObjectIntrospectionSpecification extends Specification {
98157
depth == 21 // after max depth we have nulls
99158
}
100159

101-
102160
void 'max depth is honored — list version'() {
103161
setup:
104162
def list = []
@@ -127,18 +185,18 @@ class ObjectIntrospectionSpecification extends Specification {
127185
depth == 21 // after max depth we have nulls
128186
}
129187

130-
def 'conversion of an element throws'() {
188+
void 'conversion of an element throws'() {
131189
setup:
132190
def cs = new CharSequence() {
133-
@Delegate String s = ''
191+
@Delegate String s = ''
134192

135-
@Override
136-
String toString() {
137-
throw new RuntimeException('my exception')
138-
}
193+
@Override
194+
String toString() {
195+
throw new RuntimeException('my exception')
139196
}
197+
}
140198

141199
expect:
142-
convert([cs]) == ['<error: my exception>']
200+
convert([cs]) == ['error:my exception']
143201
}
144202
}

0 commit comments

Comments
 (0)