Skip to content

Commit e2d5ed0

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 e2d5ed0

File tree

10 files changed

+210
-0
lines changed

10 files changed

+210
-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: 76 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,6 +20,9 @@
1820
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
1921
import java.io.IOException;
2022
import java.io.InputStream;
23+
import java.util.ArrayList;
24+
import java.util.Collection;
25+
import java.util.List;
2126
import java.util.function.BiFunction;
2227
import javax.servlet.ServletException;
2328
import net.bytebuddy.asm.Advice;
@@ -74,6 +79,8 @@ public void methodAdvice(MethodTransformer transformer) {
7479
.and(takesArgument(0, String.class))
7580
.or(named("getParts").and(takesArguments(0))),
7681
getClass().getName() + "$GetPartsAdvice");
82+
transformer.applyAdvice(
83+
named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice");
7784
}
7885

7986
@Override
@@ -194,6 +201,75 @@ static void muzzle(Request req) throws ServletException, IOException {
194201
}
195202
}
196203

204+
@RequiresRequestContext(RequestContextSlot.APPSEC)
205+
public static class GetFilenamesAdvice {
206+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
207+
static void after(
208+
@Advice.Return Collection parts,
209+
@ActiveRequestContext RequestContext reqCtx,
210+
@Advice.Thrown(readOnly = false) Throwable t) {
211+
if (t != null || parts == null || parts.isEmpty()) {
212+
return;
213+
}
214+
List<String> filenames = new ArrayList<>();
215+
for (Object part : parts) {
216+
String name = null;
217+
// Try Servlet 3.1+ API first (getSubmittedFileName)
218+
try {
219+
name = (String) part.getClass().getMethod("getSubmittedFileName").invoke(part);
220+
} catch (Exception ignored) {
221+
}
222+
// Fallback: parse filename from Content-Disposition header (Servlet 3.0)
223+
if (name == null) {
224+
try {
225+
String cd =
226+
(String)
227+
part.getClass()
228+
.getMethod("getHeader", String.class)
229+
.invoke(part, "content-disposition");
230+
if (cd != null) {
231+
for (String tok : cd.split(";")) {
232+
tok = tok.trim();
233+
if (tok.startsWith("filename=")) {
234+
name = tok.substring(9).trim();
235+
if (name.startsWith("\"") && name.endsWith("\"")) {
236+
name = name.substring(1, name.length() - 1);
237+
}
238+
break;
239+
}
240+
}
241+
}
242+
} catch (Exception ignored2) {
243+
}
244+
}
245+
if (name != null && !name.isEmpty()) {
246+
filenames.add(name);
247+
}
248+
}
249+
if (filenames.isEmpty()) {
250+
return;
251+
}
252+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
253+
BiFunction<RequestContext, List<String>, Flow<Void>> callback =
254+
cbp.getCallback(EVENTS.requestFilesFilenames());
255+
if (callback == null) {
256+
return;
257+
}
258+
Flow<Void> flow = callback.apply(reqCtx, filenames);
259+
Flow.Action action = flow.getAction();
260+
if (action instanceof Flow.Action.RequestBlockingAction) {
261+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
262+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
263+
if (brf != null) {
264+
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
265+
if (t == null) {
266+
t = new BlockingException("Blocked request (multipart file upload)");
267+
}
268+
}
269+
}
270+
}
271+
}
272+
197273
public static class GetPartsVisitorWrapper implements AsmVisitorWrapper {
198274
@Override
199275
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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
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;
2326
import net.bytebuddy.asm.Advice;
2427
import org.eclipse.jetty.server.Request;
@@ -48,6 +51,8 @@ public void methodAdvice(MethodTransformer transformer) {
4851
.and(takesArguments(1))
4952
.and(takesArgument(0, named("org.eclipse.jetty.util.MultiMap"))),
5053
getClass().getName() + "$GetPartsAdvice");
54+
transformer.applyAdvice(
55+
named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice");
5156
}
5257

5358
private static final Reference REQUEST_REFERENCE =
@@ -135,4 +140,49 @@ static void after(
135140
}
136141
}
137142
}
143+
144+
@RequiresRequestContext(RequestContextSlot.APPSEC)
145+
public static class GetFilenamesAdvice {
146+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
147+
static void after(
148+
@Advice.Return Collection parts,
149+
@ActiveRequestContext RequestContext reqCtx,
150+
@Advice.Thrown(readOnly = false) Throwable t) {
151+
if (t != null || parts == null || parts.isEmpty()) {
152+
return;
153+
}
154+
List<String> filenames = new ArrayList<>();
155+
for (Object part : parts) {
156+
try {
157+
String name = (String) part.getClass().getMethod("getSubmittedFileName").invoke(part);
158+
if (name != null && !name.isEmpty()) {
159+
filenames.add(name);
160+
}
161+
} catch (Exception ignored) {
162+
}
163+
}
164+
if (filenames.isEmpty()) {
165+
return;
166+
}
167+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
168+
BiFunction<RequestContext, List<String>, Flow<Void>> callback =
169+
cbp.getCallback(EVENTS.requestFilesFilenames());
170+
if (callback == null) {
171+
return;
172+
}
173+
Flow<Void> flow = callback.apply(reqCtx, filenames);
174+
Flow.Action action = flow.getAction();
175+
if (action instanceof Flow.Action.RequestBlockingAction) {
176+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
177+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
178+
if (brf != null) {
179+
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
180+
if (t == null) {
181+
t = new BlockingException("Blocked request (multipart file upload)");
182+
reqCtx.getTraceSegment().effectivelyBlocked();
183+
}
184+
}
185+
}
186+
}
187+
}
138188
}

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
import datadog.trace.api.gateway.RequestContextSlot;
1919
import datadog.trace.bootstrap.CallDepthThreadLocalMap;
2020
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
21+
import java.util.ArrayList;
22+
import java.util.Collection;
23+
import java.util.List;
2124
import java.util.function.BiFunction;
2225
import net.bytebuddy.asm.Advice;
2326
import org.eclipse.jetty.server.Request;
@@ -42,6 +45,7 @@ public void methodAdvice(MethodTransformer transformer) {
4245
transformer.applyAdvice(
4346
named("extractContentParameters").and(takesArguments(0)).or(named("getParts")),
4447
getClass().getName() + "$ExtractContentParametersAdvice");
48+
transformer.applyAdvice(named("getParts"), getClass().getName() + "$GetFilenamesAdvice");
4549
}
4650

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

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)