Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Expand Up @@ -16,14 +16,10 @@
import datadog.trace.api.gateway.RequestContext;
import datadog.trace.api.gateway.RequestContextSlot;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import net.bytebuddy.asm.Advice;
import org.jboss.resteasy.plugins.providers.multipart.InputPart;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;

@AutoService(InstrumenterModule.class)
Expand Down Expand Up @@ -67,8 +63,7 @@ public static class ReadFromAdvice {
static void after(
@Advice.Return final MultipartFormDataInput ret,
@ActiveRequestContext RequestContext reqCtx,
@Advice.Thrown(readOnly = false) Throwable t)
throws IOException {
@Advice.Thrown(readOnly = false) Throwable t) {
if (ret == null || t != null) {
return;
}
Expand All @@ -85,14 +80,7 @@ static void after(
}

if (callback != null) {
Map<String, List<String>> m = new HashMap<>();
for (Map.Entry<String, List<InputPart>> e : ret.getFormDataMap().entrySet()) {
List<String> strings = new ArrayList<>();
m.put(e.getKey(), strings);
for (InputPart inputPart : e.getValue()) {
strings.add(inputPart.getBodyAsString());
}
}
Map<String, List<String>> m = MultipartHelper.collectBodyMap(ret);

Flow<Void> flow = callback.apply(reqCtx, m);
BlockingException be =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@
import datadog.trace.api.gateway.Flow;
import datadog.trace.api.gateway.RequestContext;
import datadog.trace.api.http.MultipartContentDecoder;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.jboss.resteasy.plugins.providers.multipart.InputPart;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class MultipartHelper {

private static final Logger log = LoggerFactory.getLogger(MultipartHelper.class);

public static final int MAX_CONTENT_BYTES = Config.get().getAppSecMaxFileContentBytes();
public static final int MAX_FILES_TO_INSPECT = Config.get().getAppSecMaxFileContentCount();

Expand All @@ -35,6 +39,78 @@ private MultipartHelper() {}
GET_HEADERS = m;
}

/**
* Builds the {@code server.request.body} map out of the multipart parts.
*
* <p>Only text/plain parts are collected, matching the Jersey reference: file parts are reported
* separately via {@link #collectFilenames} / {@link #collectFilesContent} and must not also
* consume the body-map budget. The number of collected values is capped by {@link
* #MAX_FILES_TO_INSPECT}. The cap counts the total accumulated values across all field names, not
* the distinct keys: {@code getFormDataMap()} already groups parts by field name, so a per-key
* cap would be trivially bypassed by repeating the same field name on every part.
*/
public static Map<String, List<String>> collectBodyMap(MultipartFormDataInput ret) {
Map<String, List<String>> bodyMap = new HashMap<>();
int total = 0;
for (Map.Entry<String, List<InputPart>> e : ret.getFormDataMap().entrySet()) {
for (InputPart inputPart : e.getValue()) {
String contentType = contentTypeOf(inputPart);
if (!isTextPlain(contentType)) {
Comment thread
jandro996 marked this conversation as resolved.
Outdated
continue;
}
if (total >= MAX_FILES_TO_INSPECT) {
return bodyMap;
}
bodyMap
.computeIfAbsent(e.getKey(), k -> new ArrayList<>())
.add(readContent(inputPart, contentTypeWithDefaultUtf8(contentType)));
total++;
}
}
return bodyMap;
}

// No Content-Type header means RESTEasy defaults the part to text/plain (see InputPart's own
// javadoc), so a null contentType is treated as text/plain here as well.
private static boolean isTextPlain(String contentType) {
if (contentType == null) {
return true;
}
int semi = contentType.indexOf(';');
String mediaType = (semi >= 0 ? contentType.substring(0, semi) : contentType).trim();
return mediaType.equalsIgnoreCase("text/plain");
}

// Used for the body-map/text-field path only: matches Jersey's own getValue(), which decodes
// undeclared-charset text parts as UTF-8 instead of falling back to the JVM platform charset
// (MultipartContentDecoder's default for the filesContent path, kept as-is for parity with the
// other multipart integrations).
private static String contentTypeWithDefaultUtf8(String contentType) {
return MultipartContentDecoder.extractCharset(contentType) == null
? (contentType == null ? "charset=UTF-8" : contentType + "; charset=UTF-8")
: contentType;
Comment thread
jandro996 marked this conversation as resolved.
}

private static String contentTypeOf(InputPart inputPart) {
if (GET_HEADERS == null) {
return null;
}
try {
@SuppressWarnings("unchecked")
Map<String, List<String>> headers = (Map<String, List<String>>) GET_HEADERS.invoke(inputPart);
if (headers == null) {
return null;
}
List<String> ctHeaders = getHeaderCaseInsensitive(headers, "Content-Type");
return (ctHeaders != null && !ctHeaders.isEmpty()) ? ctHeaders.get(0) : null;
} catch (Exception e) {
// Reflective getHeaders() call failed (unexpected InputPart implementation): fall back to
// resolving no content-type rather than aborting the whole request's body-map collection.
log.debug("Failed to read multipart part headers via reflection", e);
return null;
}
}

public static List<String> collectFilenames(MultipartFormDataInput ret) {
List<String> filenames = new ArrayList<>();
if (GET_HEADERS == null) {
Expand Down Expand Up @@ -121,7 +197,10 @@ static String readContent(InputPart inputPart, String contentType) {
try (InputStream is = inputPart.getBody(InputStream.class, null)) {
if (is == null) return "";
return MultipartContentDecoder.readInputStream(is, MAX_CONTENT_BYTES, contentType);
} catch (IOException ignored) {
} catch (Exception e) {
// getBody()/readInputStream() can throw unchecked exceptions too (e.g. a MessageBodyReader
// lookup failure); one bad part must not abort the whole request's body/content collection.
log.debug("Failed to read multipart part content, returning empty string", e);
return "";
}
}
Expand Down
Loading
Loading