Skip to content

Commit 08405ea

Browse files
jandro996claude
andcommitted
Add server.request.body.filenames support for Undertow and Play
- Undertow: extract filenames from FormData attachments in MultiPartUploadHandlerInstrumentation - Play 2.5/2.6: extract filenames from MultipartFormData.files() in BodyParserHelpers Both implementations fire the requestFilesFilenames() IG event and support blocking on malicious filenames. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 081af53 commit 08405ea

6 files changed

Lines changed: 178 additions & 14 deletions

File tree

dd-java-agent/instrumentation/play/play-appsec-2.5/src/main/java/datadog/trace/instrumentation/play25/appsec/BodyParserHelpers.java

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,19 +106,78 @@ private static String handleText(String s) {
106106
private static MultipartFormData<?> handleMultipartFormData(MultipartFormData<?> data) {
107107
scala.collection.immutable.Map<String, Seq<String>> mpfd = data.asFormUrlEncoded();
108108

109-
if (mpfd == null || mpfd.isEmpty()) {
110-
return data;
109+
if (mpfd != null && !mpfd.isEmpty()) {
110+
try {
111+
Object conv = tryConvertingScalaContainers(mpfd, MAX_CONVERSION_DEPTH);
112+
handleArbitraryPostData(conv, "multipartFormData");
113+
} catch (Exception e) {
114+
handleException(e, "Error handling result of multipartFormData BodyParser");
115+
}
111116
}
112117

113-
try {
114-
Object conv = tryConvertingScalaContainers(mpfd, MAX_CONVERSION_DEPTH);
115-
handleArbitraryPostData(conv, "multipartFormData");
116-
} catch (Exception e) {
117-
handleException(e, "Error handling result of multipartFormData BodyParser");
118+
Seq<?> files = data.files();
119+
if (files != null && !files.isEmpty()) {
120+
try {
121+
handleMultipartFilenames(files);
122+
} catch (Exception e) {
123+
handleException(e, "Error handling multipartFormData filenames");
124+
}
118125
}
126+
119127
return data;
120128
}
121129

130+
private static void handleMultipartFilenames(Seq<?> files) {
131+
AgentSpan span = activeSpan();
132+
if (span == null) {
133+
return;
134+
}
135+
RequestContext reqCtx = span.getRequestContext();
136+
if (reqCtx == null || reqCtx.getData(RequestContextSlot.APPSEC) == null) {
137+
return;
138+
}
139+
140+
List<String> filenames = new ArrayList<>();
141+
Iterator<?> iterator = files.iterator();
142+
while (iterator.hasNext()) {
143+
MultipartFormData.FilePart<?> part = (MultipartFormData.FilePart<?>) iterator.next();
144+
String filename = part.filename();
145+
if (filename != null && !filename.isEmpty()) {
146+
filenames.add(filename);
147+
}
148+
}
149+
150+
if (filenames.isEmpty()) {
151+
return;
152+
}
153+
154+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
155+
BiFunction<RequestContext, List<String>, Flow<Void>> callback =
156+
cbp.getCallback(EVENTS.requestFilesFilenames());
157+
if (callback == null) {
158+
return;
159+
}
160+
executeFilenamesCallback(reqCtx, callback, filenames);
161+
}
162+
163+
private static void executeFilenamesCallback(
164+
RequestContext reqCtx,
165+
BiFunction<RequestContext, List<String>, Flow<Void>> callback,
166+
List<String> filenames) {
167+
Flow<Void> flow = callback.apply(reqCtx, filenames);
168+
Flow.Action action = flow.getAction();
169+
if (action instanceof Flow.Action.RequestBlockingAction) {
170+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
171+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
172+
if (brf != null) {
173+
boolean success = brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
174+
if (success) {
175+
throw new BlockingException("Blocked request (multipart file upload)");
176+
}
177+
}
178+
}
179+
}
180+
122181
public static Function1<JsValue, JsValue> getHandleJsonF() {
123182
return HANDLE_JSON;
124183
}

dd-java-agent/instrumentation/play/play-appsec-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/BodyParserHelpers.java

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,19 +117,78 @@ private static String handleText(String s) {
117117
private static MultipartFormData<?> handleMultipartFormData(MultipartFormData<?> data) {
118118
scala.collection.immutable.Map<String, Seq<String>> mpfd = data.asFormUrlEncoded();
119119

120-
if (mpfd == null || mpfd.isEmpty()) {
121-
return data;
120+
if (mpfd != null && !mpfd.isEmpty()) {
121+
try {
122+
Object conv = tryConvertingScalaContainers(mpfd, MAX_CONVERSION_DEPTH);
123+
handleArbitraryPostData(conv, "multipartFormData");
124+
} catch (Exception e) {
125+
handleException(e, "Error handling result of multipartFormData BodyParser");
126+
}
122127
}
123128

124-
try {
125-
Object conv = tryConvertingScalaContainers(mpfd, MAX_CONVERSION_DEPTH);
126-
handleArbitraryPostData(conv, "multipartFormData");
127-
} catch (Exception e) {
128-
handleException(e, "Error handling result of multipartFormData BodyParser");
129+
Seq<?> files = data.files();
130+
if (files != null && !files.isEmpty()) {
131+
try {
132+
handleMultipartFilenames(files);
133+
} catch (Exception e) {
134+
handleException(e, "Error handling multipartFormData filenames");
135+
}
129136
}
137+
130138
return data;
131139
}
132140

141+
private static void handleMultipartFilenames(Seq<?> files) {
142+
AgentSpan span = activeSpan();
143+
if (span == null) {
144+
return;
145+
}
146+
RequestContext reqCtx = span.getRequestContext();
147+
if (reqCtx == null || reqCtx.getData(RequestContextSlot.APPSEC) == null) {
148+
return;
149+
}
150+
151+
List<String> filenames = new ArrayList<>();
152+
Iterator<?> iterator = files.iterator();
153+
while (iterator.hasNext()) {
154+
MultipartFormData.FilePart<?> part = (MultipartFormData.FilePart<?>) iterator.next();
155+
String filename = part.filename();
156+
if (filename != null && !filename.isEmpty()) {
157+
filenames.add(filename);
158+
}
159+
}
160+
161+
if (filenames.isEmpty()) {
162+
return;
163+
}
164+
165+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
166+
BiFunction<RequestContext, List<String>, Flow<Void>> callback =
167+
cbp.getCallback(EVENTS.requestFilesFilenames());
168+
if (callback == null) {
169+
return;
170+
}
171+
executeFilenamesCallback(reqCtx, callback, filenames);
172+
}
173+
174+
private static void executeFilenamesCallback(
175+
RequestContext reqCtx,
176+
BiFunction<RequestContext, List<String>, Flow<Void>> callback,
177+
List<String> filenames) {
178+
Flow<Void> flow = callback.apply(reqCtx, filenames);
179+
Flow.Action action = flow.getAction();
180+
if (action instanceof Flow.Action.RequestBlockingAction) {
181+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
182+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
183+
if (brf != null) {
184+
boolean success = brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
185+
if (success) {
186+
throw new BlockingException("Blocked request (multipart file upload)");
187+
}
188+
}
189+
}
190+
}
191+
133192
public static Function1<JsValue, JsValue> getHandleJsonF() {
134193
return HANDLE_JSON;
135194
}

dd-java-agent/instrumentation/undertow/undertow-2.0/src/main/java/datadog/trace/instrumentation/undertow/MultiPartUploadHandlerInstrumentation.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
2323
import io.undertow.server.HttpServerExchange;
2424
import io.undertow.server.handlers.form.FormData;
25+
import java.util.ArrayList;
26+
import java.util.List;
2527
import java.util.function.BiFunction;
2628
import net.bytebuddy.asm.Advice;
2729

@@ -103,6 +105,35 @@ static void after(
103105
}
104106
}
105107
}
108+
109+
List<String> filenames = new ArrayList<>();
110+
for (String key : attachment) {
111+
for (FormData.FormValue formValue : attachment.get(key)) {
112+
if (formValue.isFile()) {
113+
String filename = formValue.getFileName();
114+
if (filename != null && !filename.isEmpty()) {
115+
filenames.add(filename);
116+
}
117+
}
118+
}
119+
}
120+
if (!filenames.isEmpty()) {
121+
BiFunction<RequestContext, List<String>, Flow<Void>> filenamesCb =
122+
cbp.getCallback(EVENTS.requestFilesFilenames());
123+
if (filenamesCb != null) {
124+
Flow<Void> filenamesFlow = filenamesCb.apply(reqCtx, filenames);
125+
Flow.Action filenamesAction = filenamesFlow.getAction();
126+
if (t == null && filenamesAction instanceof Flow.Action.RequestBlockingAction) {
127+
Flow.Action.RequestBlockingAction rba =
128+
(Flow.Action.RequestBlockingAction) filenamesAction;
129+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
130+
if (brf != null) {
131+
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
132+
t = new BlockingException("Blocked request (multipart file upload)");
133+
}
134+
}
135+
}
136+
}
106137
}
107138
}
108139
}

dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletAsyncTest.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@ class UndertowServletAsyncTest extends HttpServerTest<Undertow> {
295295
true
296296
}
297297

298+
@Override
299+
boolean testBodyFilenames() {
300+
true
301+
}
302+
298303
@Override
299304
boolean testBlockingOnResponse() {
300305
true

dd-java-agent/instrumentation/undertow/undertow-2.0/src/test/groovy/UndertowServletTest.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@ abstract class UndertowServletTest extends HttpServerTest<Undertow> {
196196
true
197197
}
198198

199+
@Override
200+
boolean testBodyFilenames() {
201+
true
202+
}
203+
199204
@Override
200205
boolean testBlockingOnResponse() {
201206
true

dd-java-agent/instrumentation/undertow/undertow-2.2/src/test/groovy/UndertowServletTest.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,11 @@ class UndertowServletTest extends HttpServerTest<Undertow> {
193193
true
194194
}
195195

196+
@Override
197+
boolean testBodyFilenames() {
198+
true
199+
}
200+
196201
boolean hasResponseSpan(ServerEndpoint endpoint) {
197202
// FIXME: re-enable when jakarta servlet will be fully supported
198203
// return endpoint == REDIRECT || endpoint == NOT_FOUND

0 commit comments

Comments
 (0)