Skip to content
Open
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
Expand Up @@ -12,13 +12,19 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import datadog.appsec.api.blocking.BlockingException;
import datadog.trace.api.Config;
import datadog.trace.api.gateway.BlockResponseFunction;
import datadog.trace.api.gateway.CallbackProvider;
import datadog.trace.api.gateway.Flow;
import datadog.trace.api.gateway.RequestContext;
import datadog.trace.api.gateway.RequestContextSlot;
import datadog.trace.api.http.MultipartContentDecoder;
import datadog.trace.api.internal.VisibleForTesting;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -54,13 +60,40 @@ public class BodyParserHelpers {
// cause muzzle to disable the instrumentation for Play 2.7.
private static final Method MULTIPART_FILES_METHOD;

// Cached via reflection: FilePart.ref() returns the generic file reference (TemporaryFile in
// Play 2.5/2.6), and TemporaryFile.file() returns java.io.File. Using reflection avoids
// embedding bytecode references to TemporaryFile that could break muzzle.
private static final Method FILE_PART_REF;
private static final Method TEMP_FILE_FILE;

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

static {
Method m = null;
try {
m = MultipartFormData.class.getMethod("files");
} catch (Exception ignored) {
}
MULTIPART_FILES_METHOD = m;

Method ref = null;
Method file = null;
try {
ref = MultipartFormData.FilePart.class.getMethod("ref");
} catch (Exception ignored) {
}
try {
file =
Class.forName(
"play.api.libs.Files$TemporaryFile",
false,
BodyParserHelpers.class.getClassLoader())
.getMethod("file");
} catch (Exception ignored) {
}
FILE_PART_REF = ref;
TEMP_FILE_FILE = file;
}

private static JFunction1<
Expand Down Expand Up @@ -148,6 +181,22 @@ private static MultipartFormData<?> handleMultipartFormData(MultipartFormData<?>
log.debug("Error handling multipartFormData filenames", e);
}

if (pendingBlock == null) {
try {
if (MULTIPART_FILES_METHOD != null) {
Object files = MULTIPART_FILES_METHOD.invoke(data);
if (files instanceof scala.collection.Iterable) {
handleMultipartFilesContent(
new ScalaIteratorAdapter(((scala.collection.Iterable<?>) files).iterator()));
}
}
} catch (BlockingException be) {
pendingBlock = be;
} catch (Exception e) {
log.debug("Error handling multipartFormData files content", e);
}
}

if (pendingBlock != null) throw pendingBlock;
return data;
}
Expand Down Expand Up @@ -176,6 +225,7 @@ private static void handleMultipartFilenames(java.util.Iterator<?> iterator) {
executeFilenamesCallback(reqCtx, callback, filenames);
}

@VisibleForTesting
static List<String> collectFilenames(java.util.Iterator<?> iterator) {
List<String> filenames = new ArrayList<>();
while (iterator.hasNext()) {
Expand Down Expand Up @@ -206,6 +256,91 @@ private static void executeFilenamesCallback(
}
}

private static void handleMultipartFilesContent(java.util.Iterator<?> iterator) {
AgentSpan span = activeSpan();
if (span == null) {
return;
}
RequestContext reqCtx = span.getRequestContext();
if (reqCtx == null || reqCtx.getData(RequestContextSlot.APPSEC) == null) {
return;
}

CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
BiFunction<RequestContext, List<String>, Flow<Void>> callback =
cbp.getCallback(EVENTS.requestFilesContent());
if (callback == null) {
return;
}

List<String> contents = collectFilesContent(iterator);
if (contents.isEmpty()) {
return;
}

executeFilesContentCallback(reqCtx, callback, contents);
}

@VisibleForTesting
static List<String> collectFilesContent(java.util.Iterator<?> iterator) {
List<String> contents = new ArrayList<>(MAX_FILES_TO_INSPECT);
while (iterator.hasNext() && contents.size() < MAX_FILES_TO_INSPECT) {
MultipartFormData.FilePart<?> part = (MultipartFormData.FilePart<?>) iterator.next();
// null filename → no Content-Disposition filename attribute → form field → skip
// empty filename → file upload with no name → content still inspected
if (part.filename() == null) {
continue;
}
contents.add(readFilePartContent(part));
}
return contents;
}

@VisibleForTesting
static String readFilePartContent(MultipartFormData.FilePart<?> part) {
if (FILE_PART_REF == null || TEMP_FILE_FILE == null) {
return "";
}
try {
Object ref = FILE_PART_REF.invoke(part);
if (ref == null) {
return "";
}
Object fileObj = TEMP_FILE_FILE.invoke(ref);
if (!(fileObj instanceof File)) {
return "";
}
String contentType = null;
scala.Option<?> ct = (scala.Option<?>) part.contentType();
if (ct != null && ct.isDefined()) {
contentType = (String) ct.get();
}
try (InputStream is = new FileInputStream((File) fileObj)) {
return MultipartContentDecoder.readInputStream(is, MAX_CONTENT_BYTES, contentType);
}
} catch (Exception ignored) {
return "";
}
}

private static void executeFilesContentCallback(
RequestContext reqCtx,
BiFunction<RequestContext, List<String>, Flow<Void>> callback,
List<String> contents) {
Flow<Void> flow = callback.apply(reqCtx, contents);
Flow.Action action = flow.getAction();
if (action instanceof Flow.Action.RequestBlockingAction) {
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
if (brf != null) {
boolean success = brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
if (success) {
throw new BlockingException("Blocked request (multipart file upload content)");
}
}
}
}

public static Function1<JsValue, JsValue> getHandleJsonF() {
return HANDLE_JSON;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ class PlayServerTest extends HttpServerTest<Server> {
true
}

@Override
boolean testBodyFilesContent() {
true
Comment thread
jandro996 marked this conversation as resolved.
}

@Override
boolean testBlocking() {
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import datadog.appsec.api.blocking.BlockingException;
import datadog.trace.api.Config;
import datadog.trace.api.gateway.BlockResponseFunction;
import datadog.trace.api.gateway.CallbackProvider;
import datadog.trace.api.gateway.Flow;
import datadog.trace.api.gateway.RequestContext;
import datadog.trace.api.gateway.RequestContextSlot;
import datadog.trace.api.http.MultipartContentDecoder;
import datadog.trace.api.internal.VisibleForTesting;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -64,13 +70,40 @@ public class BodyParserHelpers {
// cause muzzle to disable the instrumentation for Play 2.7.
private static final Method MULTIPART_FILES_METHOD;

// Cached via reflection: FilePart.ref() returns the generic file reference (TemporaryFile in
// Play 2.5/2.6), and TemporaryFile.file() returns java.io.File. Using reflection avoids
// embedding bytecode references to TemporaryFile that could break muzzle.
private static final Method FILE_PART_REF;
private static final Method TEMP_FILE_FILE;

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

static {
Method m = null;
try {
m = MultipartFormData.class.getMethod("files");
} catch (Exception ignored) {
}
MULTIPART_FILES_METHOD = m;

Method ref = null;
Method file = null;
try {
ref = MultipartFormData.FilePart.class.getMethod("ref");
} catch (Exception ignored) {
}
try {
file =
Class.forName(
"play.api.libs.Files$TemporaryFile",
false,
BodyParserHelpers.class.getClassLoader())
.getMethod("file");
} catch (Exception ignored) {
}
FILE_PART_REF = ref;
TEMP_FILE_FILE = file;
}

private static JFunction1<
Expand Down Expand Up @@ -159,6 +192,22 @@ private static MultipartFormData<?> handleMultipartFormData(MultipartFormData<?>
log.debug("Error handling multipartFormData filenames", e);
}

if (pendingBlock == null) {
try {
if (MULTIPART_FILES_METHOD != null) {
Object files = MULTIPART_FILES_METHOD.invoke(data);
if (files instanceof scala.collection.Iterable) {
handleMultipartFilesContent(
new ScalaIteratorAdapter(((scala.collection.Iterable<?>) files).iterator()));
}
}
} catch (BlockingException be) {
pendingBlock = be;
} catch (Exception e) {
log.debug("Error handling multipartFormData files content", e);
}
}

if (pendingBlock != null) throw pendingBlock;
return data;
}
Expand Down Expand Up @@ -187,6 +236,7 @@ private static void handleMultipartFilenames(java.util.Iterator<?> iterator) {
executeFilenamesCallback(reqCtx, callback, filenames);
}

@VisibleForTesting
static List<String> collectFilenames(java.util.Iterator<?> iterator) {
List<String> filenames = new ArrayList<>();
while (iterator.hasNext()) {
Expand Down Expand Up @@ -217,6 +267,91 @@ private static void executeFilenamesCallback(
}
}

private static void handleMultipartFilesContent(java.util.Iterator<?> iterator) {
AgentSpan span = activeSpan();
if (span == null) {
return;
}
RequestContext reqCtx = span.getRequestContext();
if (reqCtx == null || reqCtx.getData(RequestContextSlot.APPSEC) == null) {
return;
}

CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
BiFunction<RequestContext, List<String>, Flow<Void>> callback =
cbp.getCallback(EVENTS.requestFilesContent());
if (callback == null) {
return;
}

List<String> contents = collectFilesContent(iterator);
if (contents.isEmpty()) {
return;
}

executeFilesContentCallback(reqCtx, callback, contents);
}

@VisibleForTesting
static List<String> collectFilesContent(java.util.Iterator<?> iterator) {
List<String> contents = new ArrayList<>(MAX_FILES_TO_INSPECT);
while (iterator.hasNext() && contents.size() < MAX_FILES_TO_INSPECT) {
MultipartFormData.FilePart<?> part = (MultipartFormData.FilePart<?>) iterator.next();
// null filename → no Content-Disposition filename attribute → form field → skip
// empty filename → file upload with no name → content still inspected
if (part.filename() == null) {
continue;
}
contents.add(readFilePartContent(part));
}
return contents;
}

@VisibleForTesting
static String readFilePartContent(MultipartFormData.FilePart<?> part) {
if (FILE_PART_REF == null || TEMP_FILE_FILE == null) {
return "";
}
try {
Object ref = FILE_PART_REF.invoke(part);
if (ref == null) {
return "";
}
Object fileObj = TEMP_FILE_FILE.invoke(ref);
if (!(fileObj instanceof File)) {
return "";
}
String contentType = null;
scala.Option<?> ct = (scala.Option<?>) part.contentType();
if (ct != null && ct.isDefined()) {
contentType = (String) ct.get();
}
try (InputStream is = new FileInputStream((File) fileObj)) {
return MultipartContentDecoder.readInputStream(is, MAX_CONTENT_BYTES, contentType);
}
} catch (Exception ignored) {
return "";
}
}

private static void executeFilesContentCallback(
RequestContext reqCtx,
BiFunction<RequestContext, List<String>, Flow<Void>> callback,
List<String> contents) {
Flow<Void> flow = callback.apply(reqCtx, contents);
Flow.Action action = flow.getAction();
if (action instanceof Flow.Action.RequestBlockingAction) {
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
if (brf != null) {
boolean success = brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
if (success) {
throw new BlockingException("Blocked request (multipart file upload content)");
}
}
}
}

public static Function1<JsValue, JsValue> getHandleJsonF() {
return HANDLE_JSON;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ class PlayAsyncServerTest extends AbstractPlayServerTest {
true
}

@Override
boolean testBodyFilesContent() {
true
}

@Shared
def executor

Expand Down
Loading