Skip to content

Commit 629f074

Browse files
committed
Instrument Jetty for server.request.body.filenames
Add GetFilenamesAdvice to all three Jetty AppSec modules to collect uploaded file names from multipart requests and fire the requestFilesFilenames() IG callback: - jetty-appsec-8.1.3: intercepts getParts() return value; includes Content-Disposition header fallback for Servlet 3.0 (Jetty 9.0) where getSubmittedFileName() is not available - jetty-appsec-9.2: intercepts no-arg getParts() for Servlet 3.1+ - jetty-appsec-9.3: same, applies to Jetty 9.3, 10, 11 Enable testBodyFilenames() in Jetty 9.x, 10 and 11 server tests.
1 parent 1abe140 commit 629f074

File tree

10 files changed

+222
-0
lines changed

10 files changed

+222
-0
lines changed

dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import com.google.auto.service.AutoService;
99
import datadog.appsec.api.blocking.BlockingException;
10+
import datadog.trace.advice.ActiveRequestContext;
11+
import datadog.trace.advice.RequiresRequestContext;
1012
import datadog.trace.agent.tooling.Instrumenter;
1113
import datadog.trace.agent.tooling.InstrumenterModule;
1214
import datadog.trace.api.gateway.BlockResponseFunction;
@@ -18,8 +20,13 @@
1820
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
1921
import java.io.IOException;
2022
import java.io.InputStream;
23+
import java.lang.reflect.Method;
24+
import java.util.ArrayList;
25+
import java.util.Collection;
26+
import java.util.List;
2127
import java.util.function.BiFunction;
2228
import javax.servlet.ServletException;
29+
import javax.servlet.http.Part;
2330
import net.bytebuddy.asm.Advice;
2431
import net.bytebuddy.asm.AsmVisitorWrapper;
2532
import net.bytebuddy.description.field.FieldDescription;
@@ -74,6 +81,8 @@ public void methodAdvice(MethodTransformer transformer) {
7481
.and(takesArgument(0, String.class))
7582
.or(named("getParts").and(takesArguments(0))),
7683
getClass().getName() + "$GetPartsAdvice");
84+
transformer.applyAdvice(
85+
named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice");
7786
}
7887

7988
@Override
@@ -194,6 +203,77 @@ static void muzzle(Request req) throws ServletException, IOException {
194203
}
195204
}
196205

206+
@RequiresRequestContext(RequestContextSlot.APPSEC)
207+
public static class GetFilenamesAdvice {
208+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
209+
static void after(
210+
@Advice.Return Collection parts,
211+
@ActiveRequestContext RequestContext reqCtx,
212+
@Advice.Thrown(readOnly = false) Throwable t) {
213+
if (t != null || parts == null || parts.isEmpty()) {
214+
return;
215+
}
216+
// Resolve getSubmittedFileName once (Servlet 3.1+; null on Servlet 3.0)
217+
Method getSubmittedFileName = null;
218+
try {
219+
getSubmittedFileName =
220+
parts.iterator().next().getClass().getMethod("getSubmittedFileName");
221+
} catch (Exception ignored) {
222+
}
223+
List<String> filenames = new ArrayList<>();
224+
for (Object part : parts) {
225+
String name = null;
226+
// Try Servlet 3.1+ API first (getSubmittedFileName)
227+
if (getSubmittedFileName != null) {
228+
try {
229+
name = (String) getSubmittedFileName.invoke(part);
230+
} catch (Exception ignored) {
231+
}
232+
}
233+
// Fallback: parse filename from Content-Disposition header (Servlet 3.0)
234+
if (name == null) {
235+
String cd = ((Part) part).getHeader("content-disposition");
236+
if (cd != null) {
237+
for (String tok : cd.split(";")) {
238+
tok = tok.trim();
239+
if (tok.startsWith("filename=")) {
240+
name = tok.substring(9).trim();
241+
if (name.startsWith("\"") && name.endsWith("\"")) {
242+
name = name.substring(1, name.length() - 1);
243+
}
244+
break;
245+
}
246+
}
247+
}
248+
}
249+
if (name != null && !name.isEmpty()) {
250+
filenames.add(name);
251+
}
252+
}
253+
if (filenames.isEmpty()) {
254+
return;
255+
}
256+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
257+
BiFunction<RequestContext, List<String>, Flow<Void>> callback =
258+
cbp.getCallback(EVENTS.requestFilesFilenames());
259+
if (callback == null) {
260+
return;
261+
}
262+
Flow<Void> flow = callback.apply(reqCtx, filenames);
263+
Flow.Action action = flow.getAction();
264+
if (action instanceof Flow.Action.RequestBlockingAction) {
265+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
266+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
267+
if (brf != null) {
268+
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
269+
if (t == null) {
270+
t = new BlockingException("Blocked request (multipart file upload)");
271+
}
272+
}
273+
}
274+
}
275+
}
276+
197277
public static class GetPartsVisitorWrapper implements AsmVisitorWrapper {
198278
@Override
199279
public int mergeWriter(int flags) {

dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
import datadog.trace.api.gateway.RequestContextSlot;
2020
import datadog.trace.bootstrap.CallDepthThreadLocalMap;
2121
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
22+
import java.util.ArrayList;
23+
import java.util.Collection;
24+
import java.util.List;
2225
import java.util.function.BiFunction;
26+
import javax.servlet.http.Part;
2327
import net.bytebuddy.asm.Advice;
2428
import org.eclipse.jetty.server.Request;
2529
import org.eclipse.jetty.util.MultiMap;
@@ -48,6 +52,8 @@ public void methodAdvice(MethodTransformer transformer) {
4852
.and(takesArguments(1))
4953
.and(takesArgument(0, named("org.eclipse.jetty.util.MultiMap"))),
5054
getClass().getName() + "$GetPartsAdvice");
55+
transformer.applyAdvice(
56+
named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice");
5157
}
5258

5359
private static final Reference REQUEST_REFERENCE =
@@ -135,4 +141,46 @@ static void after(
135141
}
136142
}
137143
}
144+
145+
@RequiresRequestContext(RequestContextSlot.APPSEC)
146+
public static class GetFilenamesAdvice {
147+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
148+
static void after(
149+
@Advice.Return Collection parts,
150+
@ActiveRequestContext RequestContext reqCtx,
151+
@Advice.Thrown(readOnly = false) Throwable t) {
152+
if (t != null || parts == null || parts.isEmpty()) {
153+
return;
154+
}
155+
List<String> filenames = new ArrayList<>();
156+
for (Object part : parts) {
157+
String name = ((Part) part).getSubmittedFileName();
158+
if (name != null && !name.isEmpty()) {
159+
filenames.add(name);
160+
}
161+
}
162+
if (filenames.isEmpty()) {
163+
return;
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+
Flow<Void> flow = callback.apply(reqCtx, filenames);
172+
Flow.Action action = flow.getAction();
173+
if (action instanceof Flow.Action.RequestBlockingAction) {
174+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
175+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
176+
if (brf != null) {
177+
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
178+
if (t == null) {
179+
t = new BlockingException("Blocked request (multipart file upload)");
180+
reqCtx.getTraceSegment().effectivelyBlocked();
181+
}
182+
}
183+
}
184+
}
185+
}
138186
}

dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
import datadog.trace.api.gateway.RequestContextSlot;
1919
import datadog.trace.bootstrap.CallDepthThreadLocalMap;
2020
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
21+
import java.lang.reflect.Method;
22+
import java.util.ArrayList;
23+
import java.util.Collection;
24+
import java.util.List;
2125
import java.util.function.BiFunction;
2226
import net.bytebuddy.asm.Advice;
2327
import org.eclipse.jetty.server.Request;
@@ -42,6 +46,7 @@ public void methodAdvice(MethodTransformer transformer) {
4246
transformer.applyAdvice(
4347
named("extractContentParameters").and(takesArguments(0)).or(named("getParts")),
4448
getClass().getName() + "$ExtractContentParametersAdvice");
49+
transformer.applyAdvice(named("getParts"), getClass().getName() + "$GetFilenamesAdvice");
4550
}
4651

4752
private static final Reference REQUEST_REFERENCE =
@@ -99,4 +104,58 @@ static void after(
99104
}
100105
}
101106
}
107+
108+
@RequiresRequestContext(RequestContextSlot.APPSEC)
109+
public static class GetFilenamesAdvice {
110+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
111+
static void after(
112+
@Advice.Return Collection parts,
113+
@ActiveRequestContext RequestContext reqCtx,
114+
@Advice.Thrown(readOnly = false) Throwable t) {
115+
if (t != null || parts == null || parts.isEmpty()) {
116+
return;
117+
}
118+
Method getSubmittedFileName = null;
119+
try {
120+
getSubmittedFileName =
121+
parts.iterator().next().getClass().getMethod("getSubmittedFileName");
122+
} catch (Exception ignored) {
123+
}
124+
if (getSubmittedFileName == null) {
125+
return;
126+
}
127+
List<String> filenames = new ArrayList<>();
128+
for (Object part : parts) {
129+
try {
130+
String name = (String) getSubmittedFileName.invoke(part);
131+
if (name != null && !name.isEmpty()) {
132+
filenames.add(name);
133+
}
134+
} catch (Exception ignored) {
135+
}
136+
}
137+
if (filenames.isEmpty()) {
138+
return;
139+
}
140+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
141+
BiFunction<RequestContext, List<String>, Flow<Void>> callback =
142+
cbp.getCallback(EVENTS.requestFilesFilenames());
143+
if (callback == null) {
144+
return;
145+
}
146+
Flow<Void> flow = callback.apply(reqCtx, filenames);
147+
Flow.Action action = flow.getAction();
148+
if (action instanceof Flow.Action.RequestBlockingAction) {
149+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
150+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
151+
if (brf != null) {
152+
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
153+
if (t == null) {
154+
t = new BlockingException("Blocked request (multipart file upload)");
155+
reqCtx.getTraceSegment().effectivelyBlocked();
156+
}
157+
}
158+
}
159+
}
160+
}
102161
}

dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/src/test/groovy/datadog/trace/instrumentation/jetty10/Jetty10Test.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ abstract class Jetty10Test extends HttpServerTest<Server> {
8585
true
8686
}
8787

88+
@Override
89+
boolean testBodyFilenames() {
90+
true
91+
}
92+
8893
@Override
8994
boolean testSessionId() {
9095
true

dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ abstract class Jetty11Test extends HttpServerTest<Server> {
6767
true
6868
}
6969

70+
@Override
71+
boolean testBodyFilenames() {
72+
true
73+
}
74+
7075
@Override
7176
boolean testBlocking() {
7277
true

dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/JettyAsyncHandlerTest.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ class JettyAsyncHandlerTest extends Jetty11Test implements TestingGenericHttpNam
2525
false
2626
}
2727

28+
@Override
29+
boolean testBodyFilenames() {
30+
false
31+
}
32+
2833
static class ContinuationTestHandler implements Handler {
2934
@Delegate
3035
private final Handler delegate

dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0.4/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ abstract class Jetty9Test extends HttpServerTest<Server> {
8585
true
8686
}
8787

88+
@Override
89+
boolean testBodyFilenames() {
90+
true
91+
}
92+
8893
@Override
8994
boolean testSessionId() {
9095
true

dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ abstract class Jetty9Test extends HttpServerTest<Server> {
8484
true
8585
}
8686

87+
@Override
88+
boolean testBodyFilenames() {
89+
true
90+
}
91+
8792
@Override
8893
boolean testSessionId() {
8994
true

dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ abstract class Jetty9Test extends HttpServerTest<Server> {
8484
true
8585
}
8686

87+
@Override
88+
boolean testBodyFilenames() {
89+
true
90+
}
91+
8792
@Override
8893
boolean testSessionId() {
8994
true

dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ abstract class Jetty9Test extends HttpServerTest<Server> {
8585
true
8686
}
8787

88+
@Override
89+
boolean testBodyFilenames() {
90+
true
91+
}
92+
8893
@Override
8994
boolean testSessionId() {
9095
true

0 commit comments

Comments
 (0)