|
3 | 3 | import com.datadog.appsec.gateway.AppSecRequestContext;
|
4 | 4 | import datadog.trace.api.Platform;
|
5 | 5 | import datadog.trace.api.telemetry.WafMetricCollector;
|
| 6 | +import datadog.trace.util.MethodHandles; |
| 7 | +import java.lang.invoke.MethodHandle; |
6 | 8 | import java.lang.reflect.Array;
|
7 | 9 | import java.lang.reflect.Field;
|
8 | 10 | import java.lang.reflect.InvocationTargetException;
|
9 | 11 | import java.lang.reflect.Method;
|
10 | 12 | import java.lang.reflect.Modifier;
|
11 | 13 | import java.util.ArrayList;
|
12 | 14 | import java.util.HashMap;
|
| 15 | +import java.util.Iterator; |
13 | 16 | import java.util.List;
|
14 | 17 | import java.util.Map;
|
15 | 18 | import org.slf4j.Logger;
|
@@ -178,6 +181,19 @@ private static Object doConversion(Object obj, int depth, State state) {
|
178 | 181 | return obj.toString();
|
179 | 182 | }
|
180 | 183 |
|
| 184 | + // Jackson databind nodes (via reflection) |
| 185 | + Class<?> clazz = obj.getClass(); |
| 186 | + if (clazz.getName().startsWith("com.fasterxml.jackson.databind.node.")) { |
| 187 | + try { |
| 188 | + return doConversionJacksonNode( |
| 189 | + new JacksonContext(clazz.getClassLoader()), obj, depth, state); |
| 190 | + } catch (Throwable e) { |
| 191 | + // in case of failure let default conversion run |
| 192 | + log.debug("Error handling jackson node {}", clazz, e); |
| 193 | + return null; |
| 194 | + } |
| 195 | + } |
| 196 | + |
181 | 197 | // maps
|
182 | 198 | if (obj instanceof Map) {
|
183 | 199 | Map<Object, Object> newMap = new HashMap<>((int) Math.ceil(((Map) obj).size() / .75));
|
@@ -212,7 +228,6 @@ private static Object doConversion(Object obj, int depth, State state) {
|
212 | 228 | }
|
213 | 229 |
|
214 | 230 | // arrays
|
215 |
| - Class<?> clazz = obj.getClass(); |
216 | 231 | if (clazz.isArray()) {
|
217 | 232 | int length = Array.getLength(obj);
|
218 | 233 | List<Object> newList = new ArrayList<>(length);
|
@@ -305,4 +320,139 @@ private static String checkStringLength(final String str, final State state) {
|
305 | 320 | }
|
306 | 321 | return str;
|
307 | 322 | }
|
| 323 | + |
| 324 | + /** |
| 325 | + * Converts Jackson databind JsonNode objects to WAF-compatible data structures using reflection. |
| 326 | + * |
| 327 | + * <p>Jackson databind objects ({@link com.fasterxml.jackson.databind.JsonNode}) implement |
| 328 | + * iterable interfaces which interferes with the standard object introspection logic. This method |
| 329 | + * bypasses that by using reflection to directly access JsonNode internals and convert them to |
| 330 | + * appropriate data types. |
| 331 | + * |
| 332 | + * <p>Supported JsonNode types and their conversions: |
| 333 | + * |
| 334 | + * <ul> |
| 335 | + * <li>{@code OBJECT} - Converted to {@link HashMap} with string keys and recursively converted |
| 336 | + * values |
| 337 | + * <li>{@code ARRAY} - Converted to {@link ArrayList} with recursively converted elements |
| 338 | + * <li>{@code STRING} - Extracted as {@link String}, subject to length truncation |
| 339 | + * <li>{@code NUMBER} - Extracted as the appropriate {@link Number} subtype (Integer, Long, |
| 340 | + * Double, etc.) |
| 341 | + * <li>{@code BOOLEAN} - Extracted as {@link Boolean} |
| 342 | + * <li>{@code NULL}, {@code MISSING}, {@code BINARY}, {@code POJO} - Converted to {@code null} |
| 343 | + * </ul> |
| 344 | + * |
| 345 | + * <p>The method applies the same truncation limits as the main conversion logic: |
| 346 | + */ |
| 347 | + private static Object doConversionJacksonNode( |
| 348 | + final JacksonContext ctx, final Object node, final int depth, final State state) |
| 349 | + throws Throwable { |
| 350 | + if (node == null) { |
| 351 | + return null; |
| 352 | + } |
| 353 | + state.elemsLeft--; |
| 354 | + if (state.elemsLeft <= 0) { |
| 355 | + state.listMapTooLarge = true; |
| 356 | + return null; |
| 357 | + } |
| 358 | + if (depth > MAX_DEPTH) { |
| 359 | + state.objectTooDeep = true; |
| 360 | + return null; |
| 361 | + } |
| 362 | + final String type = ctx.getNodeType(node); |
| 363 | + if (type == null) { |
| 364 | + return null; |
| 365 | + } |
| 366 | + switch (type) { |
| 367 | + case "OBJECT": |
| 368 | + final Map<Object, Object> newMap = new HashMap<>(ctx.getSize(node)); |
| 369 | + for (Iterator<String> names = ctx.getFieldNames(node); names.hasNext(); ) { |
| 370 | + final String key = names.next(); |
| 371 | + final Object newKey = keyConversion(key, state); |
| 372 | + if (newKey == null && key != null) { |
| 373 | + // probably we're out of elements anyway |
| 374 | + continue; |
| 375 | + } |
| 376 | + final Object value = ctx.getField(node, key); |
| 377 | + newMap.put(newKey, doConversionJacksonNode(ctx, value, depth + 1, state)); |
| 378 | + } |
| 379 | + return newMap; |
| 380 | + case "ARRAY": |
| 381 | + final List<Object> newList = new ArrayList<>(ctx.getSize(node)); |
| 382 | + for (Object o : ((Iterable<?>) node)) { |
| 383 | + if (state.elemsLeft <= 0) { |
| 384 | + state.listMapTooLarge = true; |
| 385 | + break; |
| 386 | + } |
| 387 | + newList.add(doConversionJacksonNode(ctx, o, depth + 1, state)); |
| 388 | + } |
| 389 | + return newList; |
| 390 | + case "BOOLEAN": |
| 391 | + return ctx.getBooleanValue(node); |
| 392 | + case "NUMBER": |
| 393 | + return ctx.getNumberValue(node); |
| 394 | + case "STRING": |
| 395 | + return checkStringLength(ctx.getTextValue(node), state); |
| 396 | + default: |
| 397 | + // return null for the rest |
| 398 | + return null; |
| 399 | + } |
| 400 | + } |
| 401 | + |
| 402 | + /** |
| 403 | + * Context class used to cache method resolutions while converting a top level json node class. |
| 404 | + */ |
| 405 | + private static class JacksonContext { |
| 406 | + private final MethodHandles handles; |
| 407 | + private final Class<?> jsonNode; |
| 408 | + private MethodHandle nodeType; |
| 409 | + private MethodHandle size; |
| 410 | + private MethodHandle fieldNames; |
| 411 | + private MethodHandle fieldValue; |
| 412 | + private MethodHandle textValue; |
| 413 | + private MethodHandle booleanValue; |
| 414 | + private MethodHandle numberValue; |
| 415 | + |
| 416 | + private JacksonContext(final ClassLoader cl) throws ClassNotFoundException { |
| 417 | + handles = new MethodHandles(cl); |
| 418 | + jsonNode = cl.loadClass("com.fasterxml.jackson.databind.JsonNode"); |
| 419 | + } |
| 420 | + |
| 421 | + private String getNodeType(final Object node) throws Throwable { |
| 422 | + nodeType = nodeType == null ? handles.method(jsonNode, "getNodeType") : nodeType; |
| 423 | + final Enum<?> type = (Enum<?>) nodeType.invoke(node); |
| 424 | + return type == null ? null : type.name(); |
| 425 | + } |
| 426 | + |
| 427 | + private int getSize(final Object node) throws Throwable { |
| 428 | + size = size == null ? handles.method(jsonNode, "size") : size; |
| 429 | + return (int) size.invoke(node); |
| 430 | + } |
| 431 | + |
| 432 | + @SuppressWarnings("unchecked") |
| 433 | + private Iterator<String> getFieldNames(final Object node) throws Throwable { |
| 434 | + fieldNames = fieldNames == null ? handles.method(jsonNode, "fieldNames") : fieldNames; |
| 435 | + return (Iterator<String>) fieldNames.invoke(node); |
| 436 | + } |
| 437 | + |
| 438 | + private Object getField(final Object node, final String name) throws Throwable { |
| 439 | + fieldValue = fieldValue == null ? handles.method(jsonNode, "get", String.class) : fieldValue; |
| 440 | + return fieldValue.invoke(node, name); |
| 441 | + } |
| 442 | + |
| 443 | + private String getTextValue(final Object node) throws Throwable { |
| 444 | + textValue = textValue == null ? handles.method(jsonNode, "textValue") : textValue; |
| 445 | + return (String) textValue.invoke(node); |
| 446 | + } |
| 447 | + |
| 448 | + private Number getNumberValue(final Object node) throws Throwable { |
| 449 | + numberValue = numberValue == null ? handles.method(jsonNode, "numberValue") : numberValue; |
| 450 | + return (Number) numberValue.invoke(node); |
| 451 | + } |
| 452 | + |
| 453 | + private Boolean getBooleanValue(final Object node) throws Throwable { |
| 454 | + booleanValue = booleanValue == null ? handles.method(jsonNode, "booleanValue") : booleanValue; |
| 455 | + return (Boolean) booleanValue.invoke(node); |
| 456 | + } |
| 457 | + } |
308 | 458 | }
|
0 commit comments