Skip to content

Add String length truncation limit to ObjectIntrospector and update truncation metrics #8825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 21, 2025
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
@@ -1,6 +1,8 @@
package com.datadog.appsec.event.data;

import com.datadog.appsec.gateway.AppSecRequestContext;
import datadog.trace.api.Platform;
import datadog.trace.api.telemetry.WafMetricCollector;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
Expand All @@ -16,6 +18,7 @@
public final class ObjectIntrospection {
private static final int MAX_DEPTH = 20;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a separate PR, we should align these with WAF limits, see dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFModule.java

private static final int MAX_ELEMENTS = 256;
private static final int MAX_STRING_LENGTH = 4096;
private static final Logger log = LoggerFactory.getLogger(ObjectIntrospection.class);

private static final Method trySetAccessible;
Expand Down Expand Up @@ -60,20 +63,32 @@ private ObjectIntrospection() {}
* <p>Certain instance fields are excluded. Right now, this includes metaClass fields in Groovy
* objects and this$0 fields in inner classes.
*
* <p>Only string values are preserved. Numbers or booleans are removed, since we do not expect
* rules to detect malicious payloads in these types. An exception to this are map keys, which are
* always converted to strings.
*
* @param obj an arbitrary object
* @param requestContext the request context
* @return the converted object
*/
public static Object convert(Object obj) {
return guardedConversion(obj, 0, new State());
public static Object convert(Object obj, AppSecRequestContext requestContext) {
State state = new State(requestContext);
Object converted = guardedConversion(obj, 0, state);
if (state.stringTooLong || state.listMapTooLarge || state.objectTooDeep) {
requestContext.setWafTruncated();
WafMetricCollector.get()
.wafInputTruncated(state.stringTooLong, state.listMapTooLarge, state.objectTooDeep);
}
return converted;
}

private static class State {
int elemsLeft = MAX_ELEMENTS;
int invalidKeyId;
boolean objectTooDeep = false;
boolean listMapTooLarge = false;
boolean stringTooLong = false;
AppSecRequestContext requestContext;

private State(AppSecRequestContext requestContext) {
this.requestContext = requestContext;
}
}

private static Object guardedConversion(Object obj, int depth, State state) {
Expand All @@ -94,31 +109,48 @@ private static String keyConversion(Object key, State state) {
return "null";
}
if (key instanceof String) {
return (String) key;
return checkStringLength((String) key, state);
}
if (key instanceof Number
|| key instanceof Boolean
|| key instanceof Character
|| key instanceof CharSequence) {
return key.toString();
return checkStringLength(key.toString(), state);
}
return "invalid_key:" + (++state.invalidKeyId);
}

private static Object doConversion(Object obj, int depth, State state) {
if (obj == null) {
return null;
}
state.elemsLeft--;
if (state.elemsLeft <= 0 || obj == null || depth > MAX_DEPTH) {
if (state.elemsLeft <= 0) {
state.listMapTooLarge = true;
return null;
}

if (depth > MAX_DEPTH) {
state.objectTooDeep = true;
return null;
}

// strings, booleans and numbers are preserved
if (obj instanceof String || obj instanceof Boolean || obj instanceof Number) {
// booleans and numbers are preserved
if (obj instanceof Boolean || obj instanceof Number) {
return obj;
}

// strings are preserved, but we need to check the length
if (obj instanceof String) {
return checkStringLength((String) obj, state);
}

// char sequences are transformed just in case they are not immutable,
if (obj instanceof CharSequence) {
return checkStringLength(obj.toString(), state);
}
// single char sequences are transformed to strings for ddwaf compatibility.
if (obj instanceof CharSequence || obj instanceof Character) {
if (obj instanceof Character) {
return obj.toString();
}

Expand Down Expand Up @@ -147,6 +179,7 @@ private static Object doConversion(Object obj, int depth, State state) {
}
for (Object o : ((Iterable<?>) obj)) {
if (state.elemsLeft <= 0) {
state.listMapTooLarge = true;
break;
}
newList.add(guardedConversion(o, depth + 1, state));
Expand Down Expand Up @@ -178,6 +211,7 @@ private static Object doConversion(Object obj, int depth, State state) {
for (Field[] fields : allFields) {
for (Field f : fields) {
if (state.elemsLeft <= 0) {
state.listMapTooLarge = true;
break outer;
}
if (Modifier.isStatic(f.getModifiers())) {
Expand Down Expand Up @@ -239,4 +273,12 @@ private static boolean setAccessible(Field field) {
return false;
}
}

private static String checkStringLength(final String str, final State state) {
if (str.length() > MAX_STRING_LENGTH) {
state.stringTooLong = true;
return str.substring(0, MAX_STRING_LENGTH);
}
return str;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ private Flow<Void> onGrpcServerRequestMessage(RequestContext ctx_, Object obj) {
if (subInfo == null || subInfo.isEmpty()) {
return NoopFlow.INSTANCE;
}
Object convObj = ObjectIntrospection.convert(obj);
Object convObj = ObjectIntrospection.convert(obj, ctx);
DataBundle bundle =
new SingletonDataBundle<>(KnownAddresses.GRPC_SERVER_REQUEST_MESSAGE, convObj);
try {
Expand Down Expand Up @@ -574,7 +574,7 @@ private Flow<Void> onRequestBodyProcessed(RequestContext ctx_, Object obj) {
}
DataBundle bundle =
new SingletonDataBundle<>(
KnownAddresses.REQUEST_BODY_OBJECT, ObjectIntrospection.convert(obj));
KnownAddresses.REQUEST_BODY_OBJECT, ObjectIntrospection.convert(obj, ctx));
try {
GatewayContext gwCtx = new GatewayContext(false);
return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx);
Expand Down
Loading
Loading