From 629f0742b04535ddaf052f9d3c4da3ec000ce411 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Mon, 23 Mar 2026 14:03:57 +0100 Subject: [PATCH 01/12] 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. --- .../RequestGetPartsInstrumentation.java | 80 +++++++++++++++++++ ...tractContentParametersInstrumentation.java | 48 +++++++++++ ...tractContentParametersInstrumentation.java | 59 ++++++++++++++ .../jetty10/Jetty10Test.groovy | 5 ++ .../src/test/groovy/Jetty11Test.groovy | 5 ++ .../test/groovy/JettyAsyncHandlerTest.groovy | 5 ++ .../instrumentation/jetty9/Jetty9Test.groovy | 5 ++ .../instrumentation/jetty9/Jetty9Test.groovy | 5 ++ .../instrumentation/jetty9/Jetty9Test.groovy | 5 ++ .../instrumentation/jetty9/Jetty9Test.groovy | 5 ++ 10 files changed, 222 insertions(+) diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java index 104a3affa7c..ac6b9cef4aa 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java @@ -7,6 +7,8 @@ import com.google.auto.service.AutoService; import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.advice.ActiveRequestContext; +import datadog.trace.advice.RequiresRequestContext; import datadog.trace.agent.tooling.Instrumenter; import datadog.trace.agent.tooling.InstrumenterModule; import datadog.trace.api.gateway.BlockResponseFunction; @@ -18,8 +20,13 @@ import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import java.util.function.BiFunction; import javax.servlet.ServletException; +import javax.servlet.http.Part; import net.bytebuddy.asm.Advice; import net.bytebuddy.asm.AsmVisitorWrapper; import net.bytebuddy.description.field.FieldDescription; @@ -74,6 +81,8 @@ public void methodAdvice(MethodTransformer transformer) { .and(takesArgument(0, String.class)) .or(named("getParts").and(takesArguments(0))), getClass().getName() + "$GetPartsAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice"); } @Override @@ -194,6 +203,77 @@ static void muzzle(Request req) throws ServletException, IOException { } } + @RequiresRequestContext(RequestContextSlot.APPSEC) + public static class GetFilenamesAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Return Collection parts, + @ActiveRequestContext RequestContext reqCtx, + @Advice.Thrown(readOnly = false) Throwable t) { + if (t != null || parts == null || parts.isEmpty()) { + return; + } + // Resolve getSubmittedFileName once (Servlet 3.1+; null on Servlet 3.0) + Method getSubmittedFileName = null; + try { + getSubmittedFileName = + parts.iterator().next().getClass().getMethod("getSubmittedFileName"); + } catch (Exception ignored) { + } + List filenames = new ArrayList<>(); + for (Object part : parts) { + String name = null; + // Try Servlet 3.1+ API first (getSubmittedFileName) + if (getSubmittedFileName != null) { + try { + name = (String) getSubmittedFileName.invoke(part); + } catch (Exception ignored) { + } + } + // Fallback: parse filename from Content-Disposition header (Servlet 3.0) + if (name == null) { + String cd = ((Part) part).getHeader("content-disposition"); + if (cd != null) { + for (String tok : cd.split(";")) { + tok = tok.trim(); + if (tok.startsWith("filename=")) { + name = tok.substring(9).trim(); + if (name.startsWith("\"") && name.endsWith("\"")) { + name = name.substring(1, name.length() - 1); + } + break; + } + } + } + } + if (name != null && !name.isEmpty()) { + filenames.add(name); + } + } + if (filenames.isEmpty()) { + return; + } + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction, Flow> callback = + cbp.getCallback(EVENTS.requestFilesFilenames()); + if (callback == null) { + return; + } + Flow flow = callback.apply(reqCtx, filenames); + 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) { + brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba); + if (t == null) { + t = new BlockingException("Blocked request (multipart file upload)"); + } + } + } + } + } + public static class GetPartsVisitorWrapper implements AsmVisitorWrapper { @Override public int mergeWriter(int flags) { diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java index 0796aa32538..dcfd5380e72 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java @@ -19,7 +19,11 @@ import datadog.trace.api.gateway.RequestContextSlot; import datadog.trace.bootstrap.CallDepthThreadLocalMap; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import java.util.function.BiFunction; +import javax.servlet.http.Part; import net.bytebuddy.asm.Advice; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.MultiMap; @@ -48,6 +52,8 @@ public void methodAdvice(MethodTransformer transformer) { .and(takesArguments(1)) .and(takesArgument(0, named("org.eclipse.jetty.util.MultiMap"))), getClass().getName() + "$GetPartsAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice"); } private static final Reference REQUEST_REFERENCE = @@ -135,4 +141,46 @@ static void after( } } } + + @RequiresRequestContext(RequestContextSlot.APPSEC) + public static class GetFilenamesAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Return Collection parts, + @ActiveRequestContext RequestContext reqCtx, + @Advice.Thrown(readOnly = false) Throwable t) { + if (t != null || parts == null || parts.isEmpty()) { + return; + } + List filenames = new ArrayList<>(); + for (Object part : parts) { + String name = ((Part) part).getSubmittedFileName(); + if (name != null && !name.isEmpty()) { + filenames.add(name); + } + } + if (filenames.isEmpty()) { + return; + } + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction, Flow> callback = + cbp.getCallback(EVENTS.requestFilesFilenames()); + if (callback == null) { + return; + } + Flow flow = callback.apply(reqCtx, filenames); + 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) { + brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba); + if (t == null) { + t = new BlockingException("Blocked request (multipart file upload)"); + reqCtx.getTraceSegment().effectivelyBlocked(); + } + } + } + } + } } diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java index 3e1e2bf6d5c..1af3880b9ee 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java @@ -18,6 +18,10 @@ import datadog.trace.api.gateway.RequestContextSlot; import datadog.trace.bootstrap.CallDepthThreadLocalMap; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import java.util.function.BiFunction; import net.bytebuddy.asm.Advice; import org.eclipse.jetty.server.Request; @@ -42,6 +46,7 @@ public void methodAdvice(MethodTransformer transformer) { transformer.applyAdvice( named("extractContentParameters").and(takesArguments(0)).or(named("getParts")), getClass().getName() + "$ExtractContentParametersAdvice"); + transformer.applyAdvice(named("getParts"), getClass().getName() + "$GetFilenamesAdvice"); } private static final Reference REQUEST_REFERENCE = @@ -99,4 +104,58 @@ static void after( } } } + + @RequiresRequestContext(RequestContextSlot.APPSEC) + public static class GetFilenamesAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Return Collection parts, + @ActiveRequestContext RequestContext reqCtx, + @Advice.Thrown(readOnly = false) Throwable t) { + if (t != null || parts == null || parts.isEmpty()) { + return; + } + Method getSubmittedFileName = null; + try { + getSubmittedFileName = + parts.iterator().next().getClass().getMethod("getSubmittedFileName"); + } catch (Exception ignored) { + } + if (getSubmittedFileName == null) { + return; + } + List filenames = new ArrayList<>(); + for (Object part : parts) { + try { + String name = (String) getSubmittedFileName.invoke(part); + if (name != null && !name.isEmpty()) { + filenames.add(name); + } + } catch (Exception ignored) { + } + } + if (filenames.isEmpty()) { + return; + } + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction, Flow> callback = + cbp.getCallback(EVENTS.requestFilesFilenames()); + if (callback == null) { + return; + } + Flow flow = callback.apply(reqCtx, filenames); + 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) { + brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba); + if (t == null) { + t = new BlockingException("Blocked request (multipart file upload)"); + reqCtx.getTraceSegment().effectivelyBlocked(); + } + } + } + } + } } diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/src/test/groovy/datadog/trace/instrumentation/jetty10/Jetty10Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/src/test/groovy/datadog/trace/instrumentation/jetty10/Jetty10Test.groovy index 7dec61c223f..fe040e086e4 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/src/test/groovy/datadog/trace/instrumentation/jetty10/Jetty10Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/src/test/groovy/datadog/trace/instrumentation/jetty10/Jetty10Test.groovy @@ -85,6 +85,11 @@ abstract class Jetty10Test extends HttpServerTest { true } + @Override + boolean testBodyFilenames() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy index f4da48aaaf3..a46a98a692c 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy @@ -67,6 +67,11 @@ abstract class Jetty11Test extends HttpServerTest { true } + @Override + boolean testBodyFilenames() { + true + } + @Override boolean testBlocking() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/JettyAsyncHandlerTest.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/JettyAsyncHandlerTest.groovy index 38f5b1449ab..bd1e1bb9ecc 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/JettyAsyncHandlerTest.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/JettyAsyncHandlerTest.groovy @@ -25,6 +25,11 @@ class JettyAsyncHandlerTest extends Jetty11Test implements TestingGenericHttpNam false } + @Override + boolean testBodyFilenames() { + false + } + static class ContinuationTestHandler implements Handler { @Delegate private final Handler delegate diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0.4/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0.4/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy index 38eb20340c6..6273d0f63f3 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0.4/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0.4/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy @@ -85,6 +85,11 @@ abstract class Jetty9Test extends HttpServerTest { true } + @Override + boolean testBodyFilenames() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy index 32a1b300c28..c90d9002e57 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy @@ -84,6 +84,11 @@ abstract class Jetty9Test extends HttpServerTest { true } + @Override + boolean testBodyFilenames() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy index 32a1b300c28..c90d9002e57 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy @@ -84,6 +84,11 @@ abstract class Jetty9Test extends HttpServerTest { true } + @Override + boolean testBodyFilenames() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy index 38eb20340c6..6273d0f63f3 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy @@ -85,6 +85,11 @@ abstract class Jetty9Test extends HttpServerTest { true } + @Override + boolean testBodyFilenames() { + true + } + @Override boolean testSessionId() { true From b1ec26bafa0c02ce69730210ee932e45678d70ff Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Mon, 6 Apr 2026 13:55:45 +0200 Subject: [PATCH 02/12] Fix GetFilenamesAdvice double-firing and extend coverage to getParts(MultiMap) path - jetty-appsec-9.3: add call-depth guard (Collection.class) to GetFilenamesAdvice to prevent double callback invocation when getParts() calls getParts(MultiMap) internally - jetty-appsec-9.2: extend GetFilenamesAdvice matcher to all getParts overloads (not just no-arg) to cover getParameter*()/getParameterMap() code paths, guarded with same call-depth mechanism to avoid double-firing --- .../RequestGetPartsInstrumentation.java | 28 ++++++++++--------- ...tractContentParametersInstrumentation.java | 14 ++++++++-- ...tractContentParametersInstrumentation.java | 14 ++++++++-- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java index ac6b9cef4aa..8937ead2e88 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java @@ -216,39 +216,41 @@ static void after( // Resolve getSubmittedFileName once (Servlet 3.1+; null on Servlet 3.0) Method getSubmittedFileName = null; try { - getSubmittedFileName = - parts.iterator().next().getClass().getMethod("getSubmittedFileName"); + getSubmittedFileName = parts.iterator().next().getClass().getMethod("getSubmittedFileName"); } catch (Exception ignored) { } List filenames = new ArrayList<>(); - for (Object part : parts) { - String name = null; - // Try Servlet 3.1+ API first (getSubmittedFileName) - if (getSubmittedFileName != null) { + if (getSubmittedFileName != null) { + // Servlet 3.1+: use getSubmittedFileName + for (Object part : parts) { try { - name = (String) getSubmittedFileName.invoke(part); + String name = (String) getSubmittedFileName.invoke(part); + if (name != null && !name.isEmpty()) { + filenames.add(name); + } } catch (Exception ignored) { } } - // Fallback: parse filename from Content-Disposition header (Servlet 3.0) - if (name == null) { + } else { + // Servlet 3.0: parse filename from Content-Disposition header + for (Object part : parts) { String cd = ((Part) part).getHeader("content-disposition"); if (cd != null) { for (String tok : cd.split(";")) { tok = tok.trim(); if (tok.startsWith("filename=")) { - name = tok.substring(9).trim(); + String name = tok.substring(9).trim(); if (name.startsWith("\"") && name.endsWith("\"")) { name = name.substring(1, name.length() - 1); } + if (!name.isEmpty()) { + filenames.add(name); + } break; } } } } - if (name != null && !name.isEmpty()) { - filenames.add(name); - } } if (filenames.isEmpty()) { return; diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java index dcfd5380e72..0e4de822771 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java @@ -52,8 +52,7 @@ public void methodAdvice(MethodTransformer transformer) { .and(takesArguments(1)) .and(takesArgument(0, named("org.eclipse.jetty.util.MultiMap"))), getClass().getName() + "$GetPartsAdvice"); - transformer.applyAdvice( - named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice"); + transformer.applyAdvice(named("getParts"), getClass().getName() + "$GetFilenamesAdvice"); } private static final Reference REQUEST_REFERENCE = @@ -144,12 +143,21 @@ static void after( @RequiresRequestContext(RequestContextSlot.APPSEC) public static class GetFilenamesAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static boolean before( + @Advice.FieldValue("_contentParameters") final MultiMap map) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Collection.class); + return callDepth == 0 && map == null; + } + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) static void after( + @Advice.Enter boolean proceed, @Advice.Return Collection parts, @ActiveRequestContext RequestContext reqCtx, @Advice.Thrown(readOnly = false) Throwable t) { - if (t != null || parts == null || parts.isEmpty()) { + CallDepthThreadLocalMap.decrementCallDepth(Collection.class); + if (!proceed || t != null || parts == null || parts.isEmpty()) { return; } List filenames = new ArrayList<>(); diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java index 1af3880b9ee..be87530417f 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java @@ -107,18 +107,26 @@ static void after( @RequiresRequestContext(RequestContextSlot.APPSEC) public static class GetFilenamesAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static boolean before( + @Advice.FieldValue("_contentParameters") final MultiMap map) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Collection.class); + return callDepth == 0 && map == null; + } + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) static void after( + @Advice.Enter boolean proceed, @Advice.Return Collection parts, @ActiveRequestContext RequestContext reqCtx, @Advice.Thrown(readOnly = false) Throwable t) { - if (t != null || parts == null || parts.isEmpty()) { + CallDepthThreadLocalMap.decrementCallDepth(Collection.class); + if (!proceed || t != null || parts == null || parts.isEmpty()) { return; } Method getSubmittedFileName = null; try { - getSubmittedFileName = - parts.iterator().next().getClass().getMethod("getSubmittedFileName"); + getSubmittedFileName = parts.iterator().next().getClass().getMethod("getSubmittedFileName"); } catch (Exception ignored) { } if (getSubmittedFileName == null) { From d8a92f8c6d804e05de1311a650ab2d9b19a6ddf9 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Tue, 7 Apr 2026 09:29:07 +0200 Subject: [PATCH 03/12] spotless --- .../agent/test/base/HttpServerTest.groovy | 32 +++++++ ...tractContentParametersInstrumentation.java | 3 +- ...tractContentParametersInstrumentation.java | 84 ++++++++++++++++++- .../src/test/groovy/Jetty11Test.groovy | 5 ++ .../servlet5/TestServlet5.groovy | 9 ++ 5 files changed, 128 insertions(+), 5 deletions(-) diff --git a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy index 0b2a28d954b..7c209c9c97c 100644 --- a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy +++ b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy @@ -368,6 +368,10 @@ abstract class HttpServerTest extends WithHttpServer { false } + boolean testBodyFilenamesCalledOnce() { + false + } + boolean testBodyFilenames() { false } @@ -476,6 +480,7 @@ abstract class HttpServerTest extends WithHttpServer { CREATED_IS("created_input_stream", 201, "created"), BODY_URLENCODED("body-urlencoded?ignore=pair", 200, '[a:[x]]'), BODY_MULTIPART("body-multipart?ignore=pair", 200, '[a:[x]]'), + BODY_MULTIPART_REPEATED("body-multipart-repeated", 200, "ok"), BODY_JSON("body-json", 200, '{"a":"x"}'), BODY_XML("body-xml", 200, 'mytext'), REDIRECT("redirect", 302, "/redirected"), @@ -1646,6 +1651,30 @@ abstract class HttpServerTest extends WithHttpServer { response.close() } + def 'test instrumentation gateway file upload filenames called once'() { + setup: + assumeTrue(testBodyFilenamesCalledOnce()) + RequestBody fileBody = RequestBody.create(MediaType.parse('application/octet-stream'), 'file content') + def body = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart('file', 'evil.php', fileBody) + .build() + def httpRequest = request(BODY_MULTIPART_REPEATED, 'POST', body).build() + def response = client.newCall(httpRequest).execute() + + when: + TEST_WRITER.waitForTraces(1) + + then: + TEST_WRITER.get(0).any { + it.getTag('request.body.filenames') == "[evil.php]" + && it.getTag('_dd.appsec.filenames.cb.calls') == 1 + } + + cleanup: + response.close() + } + def 'test instrumentation gateway json request body'() { setup: assumeTrue(testBodyJson()) @@ -2581,6 +2610,7 @@ abstract class HttpServerTest extends WithHttpServer { boolean responseBodyTag Object responseBody List uploadedFilenames + int uploadedFilenamesCallCount = 0 } static final String stringOrEmpty(String string) { @@ -2754,6 +2784,8 @@ abstract class HttpServerTest extends WithHttpServer { rqCtxt.traceSegment.setTagTop('request.body.filenames', filenames as String) Context context = rqCtxt.getData(RequestContextSlot.APPSEC) context.uploadedFilenames = filenames + context.uploadedFilenamesCallCount++ + rqCtxt.traceSegment.setTagTop('_dd.appsec.filenames.cb.calls', context.uploadedFilenamesCallCount) Flow.ResultFlow.empty() } as BiFunction, Flow>) diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java index 0e4de822771..1835f6ffd0c 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java @@ -144,8 +144,7 @@ static void after( @RequiresRequestContext(RequestContextSlot.APPSEC) public static class GetFilenamesAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - static boolean before( - @Advice.FieldValue("_contentParameters") final MultiMap map) { + static boolean before(@Advice.FieldValue("_contentParameters") final MultiMap map) { final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Collection.class); return callDepth == 0 && map == null; } diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java index be87530417f..888f61f8f70 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java @@ -46,7 +46,12 @@ public void methodAdvice(MethodTransformer transformer) { transformer.applyAdvice( named("extractContentParameters").and(takesArguments(0)).or(named("getParts")), getClass().getName() + "$ExtractContentParametersAdvice"); - transformer.applyAdvice(named("getParts"), getClass().getName() + "$GetFilenamesAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(0)), + getClass().getName() + "$GetFilenamesAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(1)), + getClass().getName() + "$GetFilenamesFromMultiPartAdvice"); } private static final Reference REQUEST_REFERENCE = @@ -105,11 +110,15 @@ static void after( } } + /** + * Fires the {@code requestFilesFilenames} event when the application calls public {@code + * getParts()}. The {@code _contentParameters == null} guard ensures the WAF is invoked only on + * the first call — subsequent calls return the cached result without re-processing. + */ @RequiresRequestContext(RequestContextSlot.APPSEC) public static class GetFilenamesAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - static boolean before( - @Advice.FieldValue("_contentParameters") final MultiMap map) { + static boolean before(@Advice.FieldValue("_contentParameters") final MultiMap map) { final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Collection.class); return callDepth == 0 && map == null; } @@ -166,4 +175,73 @@ static void after( } } } + + /** + * Fires the {@code requestFilesFilenames} event when multipart content is parsed via the + * internal {@code getParts(MultiMap)} path triggered by {@code getParameter*()} / + * {@code getParameterMap()} — i.e. when the application never calls public {@code getParts()}. + * In Jetty 9.3+, {@code extractContentParameters()} assigns {@code _contentParameters} before + * calling this method, so {@code map == null} cannot be used as a "first parse" guard here; + * the call-depth guard prevents double-firing when {@code getParts()} internally delegates to + * this method. + */ + @RequiresRequestContext(RequestContextSlot.APPSEC) + public static class GetFilenamesFromMultiPartAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static boolean before() { + return CallDepthThreadLocalMap.incrementCallDepth(Collection.class) == 0; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Enter boolean proceed, + @Advice.Return Collection parts, + @ActiveRequestContext RequestContext reqCtx, + @Advice.Thrown(readOnly = false) Throwable t) { + CallDepthThreadLocalMap.decrementCallDepth(Collection.class); + if (!proceed || t != null || parts == null || parts.isEmpty()) { + return; + } + Method getSubmittedFileName = null; + try { + getSubmittedFileName = parts.iterator().next().getClass().getMethod("getSubmittedFileName"); + } catch (Exception ignored) { + } + if (getSubmittedFileName == null) { + return; + } + List filenames = new ArrayList<>(); + for (Object part : parts) { + try { + String name = (String) getSubmittedFileName.invoke(part); + if (name != null && !name.isEmpty()) { + filenames.add(name); + } + } catch (Exception ignored) { + } + } + if (filenames.isEmpty()) { + return; + } + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction, Flow> callback = + cbp.getCallback(EVENTS.requestFilesFilenames()); + if (callback == null) { + return; + } + Flow flow = callback.apply(reqCtx, filenames); + 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) { + brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba); + if (t == null) { + t = new BlockingException("Blocked request (multipart file upload)"); + reqCtx.getTraceSegment().effectivelyBlocked(); + } + } + } + } + } } diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy index a46a98a692c..1fa547c761c 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy @@ -72,6 +72,11 @@ abstract class Jetty11Test extends HttpServerTest { true } + @Override + boolean testBodyFilenamesCalledOnce() { + true + } + @Override boolean testBlocking() { true diff --git a/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet5/TestServlet5.groovy b/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet5/TestServlet5.groovy index 51e7c974f6d..f9597bbbd70 100644 --- a/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet5/TestServlet5.groovy +++ b/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet5/TestServlet5.groovy @@ -11,6 +11,7 @@ import java.lang.reflect.Field import java.lang.reflect.Modifier import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART_REPEATED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_URLENCODED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CREATED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CREATED_IS @@ -68,6 +69,14 @@ class TestServlet5 extends HttpServlet { resp.status = endpoint.status resp.writer.print(req.getHeader("x-forwarded-for")) break + case BODY_MULTIPART_REPEATED: + resp.status = endpoint.status + // Call getParts() 3 times to verify the filenames callback fires only once + req.getParts() + req.getParts() + req.getParts() + resp.writer.print(endpoint.body) + break case BODY_MULTIPART: case BODY_URLENCODED: resp.status = endpoint.status From 30bb769442f292ce09d73a498241f4ac941c8b99 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Tue, 7 Apr 2026 12:01:58 +0200 Subject: [PATCH 04/12] Extend testBodyFilenamesCalledOnce coverage to Jetty 9.x and 10.x - Add BODY_MULTIPART_REPEATED case to TestServlet3 (javax) so Jetty 9.x/10.x test modules can exercise the repeated getParts() scenario - Enable testBodyFilenamesCalledOnce() for Jetty 9.0, 9.0.4, 9.3, 9.4.21, and 10.0 --- .../trace/instrumentation/jetty10/Jetty10Test.groovy | 5 +++++ .../trace/instrumentation/jetty9/Jetty9Test.groovy | 5 +++++ .../trace/instrumentation/jetty9/Jetty9Test.groovy | 5 +++++ .../trace/instrumentation/jetty9/Jetty9Test.groovy | 5 +++++ .../trace/instrumentation/jetty9/Jetty9Test.groovy | 5 +++++ .../trace/instrumentation/servlet3/TestServlet3.groovy | 9 +++++++++ 6 files changed, 34 insertions(+) diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/src/test/groovy/datadog/trace/instrumentation/jetty10/Jetty10Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/src/test/groovy/datadog/trace/instrumentation/jetty10/Jetty10Test.groovy index fe040e086e4..6e0c0f8fc20 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/src/test/groovy/datadog/trace/instrumentation/jetty10/Jetty10Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/src/test/groovy/datadog/trace/instrumentation/jetty10/Jetty10Test.groovy @@ -90,6 +90,11 @@ abstract class Jetty10Test extends HttpServerTest { true } + @Override + boolean testBodyFilenamesCalledOnce() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0.4/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0.4/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy index 6273d0f63f3..8f5d980bc10 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0.4/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0.4/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy @@ -90,6 +90,11 @@ abstract class Jetty9Test extends HttpServerTest { true } + @Override + boolean testBodyFilenamesCalledOnce() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy index c90d9002e57..d28c6aea45d 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy @@ -89,6 +89,11 @@ abstract class Jetty9Test extends HttpServerTest { true } + @Override + boolean testBodyFilenamesCalledOnce() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy index c90d9002e57..d28c6aea45d 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy @@ -89,6 +89,11 @@ abstract class Jetty9Test extends HttpServerTest { true } + @Override + boolean testBodyFilenamesCalledOnce() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy index 6273d0f63f3..8f5d980bc10 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy @@ -90,6 +90,11 @@ abstract class Jetty9Test extends HttpServerTest { true } + @Override + boolean testBodyFilenamesCalledOnce() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet3/TestServlet3.groovy b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet3/TestServlet3.groovy index 4b0b9df85d4..b3b7888bd5c 100644 --- a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet3/TestServlet3.groovy +++ b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet3/TestServlet3.groovy @@ -15,6 +15,7 @@ import java.lang.reflect.Modifier import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_URLENCODED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART_REPEATED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CREATED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CREATED_IS import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CUSTOM_EXCEPTION @@ -95,6 +96,14 @@ class TestServlet3 { resp.status = endpoint.status resp.writer.print(endpoint.bodyForQuery(req.queryString)) break + case BODY_MULTIPART_REPEATED: + resp.status = endpoint.status + // Call getParts() 3 times to verify the filenames callback fires only once + req.getParts() + req.getParts() + req.getParts() + resp.writer.print(endpoint.body) + break case BODY_URLENCODED: case BODY_MULTIPART: resp.status = endpoint.status From abddcfa6679949d928f373735065e112957b20e5 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 8 Apr 2026 12:11:14 +0200 Subject: [PATCH 05/12] Add BODY_MULTIPART_COMBINED test to cover GetFilenamesFromMultiPartAdvice path - New BODY_MULTIPART_COMBINED endpoint: calls getParameterMap() first (triggers GetFilenamesFromMultiPartAdvice via extractContentParameters -> getParts(MultiMap)), then getParts() explicitly (GetFilenamesAdvice must not double-fire since _contentParameters is already set) - New test 'file upload filenames called once via parameter map' verifies the callback fires exactly once across both advice paths - Enabled in Jetty 9.0, 9.0.4, 9.3, 9.4.21, 10.0 and 11.0 --- .../agent/test/base/HttpServerTest.groovy | 29 +++++++++++++++++++ .../jetty10/Jetty10Test.groovy | 5 ++++ .../src/test/groovy/Jetty11Test.groovy | 5 ++++ .../instrumentation/jetty9/Jetty9Test.groovy | 5 ++++ .../instrumentation/jetty9/Jetty9Test.groovy | 5 ++++ .../instrumentation/jetty9/Jetty9Test.groovy | 5 ++++ .../instrumentation/jetty9/Jetty9Test.groovy | 5 ++++ .../servlet5/TestServlet5.groovy | 11 ++++++- .../servlet3/TestServlet3.groovy | 9 ++++++ 9 files changed, 78 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy index 7c209c9c97c..b7d07ea50d1 100644 --- a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy +++ b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy @@ -372,6 +372,10 @@ abstract class HttpServerTest extends WithHttpServer { false } + boolean testBodyFilenamesCalledOnceCombined() { + false + } + boolean testBodyFilenames() { false } @@ -481,6 +485,7 @@ abstract class HttpServerTest extends WithHttpServer { BODY_URLENCODED("body-urlencoded?ignore=pair", 200, '[a:[x]]'), BODY_MULTIPART("body-multipart?ignore=pair", 200, '[a:[x]]'), BODY_MULTIPART_REPEATED("body-multipart-repeated", 200, "ok"), + BODY_MULTIPART_COMBINED("body-multipart-combined", 200, "ok"), BODY_JSON("body-json", 200, '{"a":"x"}'), BODY_XML("body-xml", 200, 'mytext'), REDIRECT("redirect", 302, "/redirected"), @@ -1675,6 +1680,30 @@ abstract class HttpServerTest extends WithHttpServer { response.close() } + def 'test instrumentation gateway file upload filenames called once via parameter map'() { + setup: + assumeTrue(testBodyFilenamesCalledOnceCombined()) + RequestBody fileBody = RequestBody.create(MediaType.parse('application/octet-stream'), 'file content') + def body = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart('file', 'evil.php', fileBody) + .build() + def httpRequest = request(BODY_MULTIPART_COMBINED, 'POST', body).build() + def response = client.newCall(httpRequest).execute() + + when: + TEST_WRITER.waitForTraces(1) + + then: + TEST_WRITER.get(0).any { + it.getTag('request.body.filenames') == "[evil.php]" + && it.getTag('_dd.appsec.filenames.cb.calls') == 1 + } + + cleanup: + response.close() + } + def 'test instrumentation gateway json request body'() { setup: assumeTrue(testBodyJson()) diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/src/test/groovy/datadog/trace/instrumentation/jetty10/Jetty10Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/src/test/groovy/datadog/trace/instrumentation/jetty10/Jetty10Test.groovy index 6e0c0f8fc20..2726574ec83 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/src/test/groovy/datadog/trace/instrumentation/jetty10/Jetty10Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/src/test/groovy/datadog/trace/instrumentation/jetty10/Jetty10Test.groovy @@ -95,6 +95,11 @@ abstract class Jetty10Test extends HttpServerTest { true } + @Override + boolean testBodyFilenamesCalledOnceCombined() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy index 1fa547c761c..80afb31077a 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/src/test/groovy/Jetty11Test.groovy @@ -77,6 +77,11 @@ abstract class Jetty11Test extends HttpServerTest { true } + @Override + boolean testBodyFilenamesCalledOnceCombined() { + true + } + @Override boolean testBlocking() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0.4/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0.4/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy index 8f5d980bc10..8a18ccbc652 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0.4/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0.4/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy @@ -95,6 +95,11 @@ abstract class Jetty9Test extends HttpServerTest { true } + @Override + boolean testBodyFilenamesCalledOnceCombined() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy index d28c6aea45d..9bdc9e1e469 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.0/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy @@ -94,6 +94,11 @@ abstract class Jetty9Test extends HttpServerTest { true } + @Override + boolean testBodyFilenamesCalledOnceCombined() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy index d28c6aea45d..9bdc9e1e469 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy @@ -94,6 +94,11 @@ abstract class Jetty9Test extends HttpServerTest { true } + @Override + boolean testBodyFilenamesCalledOnceCombined() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy index 8f5d980bc10..8a18ccbc652 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/src/test/groovy/datadog/trace/instrumentation/jetty9/Jetty9Test.groovy @@ -95,6 +95,11 @@ abstract class Jetty9Test extends HttpServerTest { true } + @Override + boolean testBodyFilenamesCalledOnceCombined() { + true + } + @Override boolean testSessionId() { true diff --git a/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet5/TestServlet5.groovy b/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet5/TestServlet5.groovy index f9597bbbd70..93060644456 100644 --- a/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet5/TestServlet5.groovy +++ b/dd-java-agent/instrumentation/servlet/jakarta-servlet-5.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet5/TestServlet5.groovy @@ -11,6 +11,7 @@ import java.lang.reflect.Field import java.lang.reflect.Modifier import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART_COMBINED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART_REPEATED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_URLENCODED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CREATED @@ -71,12 +72,20 @@ class TestServlet5 extends HttpServlet { break case BODY_MULTIPART_REPEATED: resp.status = endpoint.status - // Call getParts() 3 times to verify the filenames callback fires only once + // Call getParts() 3 times to verify the filenames callback fires only once req.getParts() req.getParts() req.getParts() resp.writer.print(endpoint.body) break + case BODY_MULTIPART_COMBINED: + resp.status = endpoint.status + // Call getParameterMap() first (exercises GetFilenamesFromMultiPartAdvice via extractContentParameters), + // then getParts() explicitly (GetFilenamesAdvice must not double-fire since map is already set) + req.parameterMap + req.getParts() + resp.writer.print(endpoint.body) + break case BODY_MULTIPART: case BODY_URLENCODED: resp.status = endpoint.status diff --git a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet3/TestServlet3.groovy b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet3/TestServlet3.groovy index b3b7888bd5c..98a5983a36d 100644 --- a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet3/TestServlet3.groovy +++ b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-3.0/src/testFixtures/groovy/datadog/trace/instrumentation/servlet3/TestServlet3.groovy @@ -15,6 +15,7 @@ import java.lang.reflect.Modifier import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_URLENCODED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART_COMBINED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART_REPEATED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CREATED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CREATED_IS @@ -104,6 +105,14 @@ class TestServlet3 { req.getParts() resp.writer.print(endpoint.body) break + case BODY_MULTIPART_COMBINED: + resp.status = endpoint.status + // Call getParameterMap() first (exercises GetFilenamesFromMultiPartAdvice via extractContentParameters), + // then getParts() explicitly (GetFilenamesAdvice must not double-fire since map is already set) + req.parameterMap + req.getParts() + resp.writer.print(endpoint.body) + break case BODY_URLENCODED: case BODY_MULTIPART: resp.status = endpoint.status From eeab933478346f2f2e095a40ef27cfd3f8492950 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 8 Apr 2026 12:13:10 +0200 Subject: [PATCH 06/12] spotless --- ...tExtractContentParametersInstrumentation.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java index 888f61f8f70..9404e0ad436 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java @@ -47,8 +47,7 @@ public void methodAdvice(MethodTransformer transformer) { named("extractContentParameters").and(takesArguments(0)).or(named("getParts")), getClass().getName() + "$ExtractContentParametersAdvice"); transformer.applyAdvice( - named("getParts").and(takesArguments(0)), - getClass().getName() + "$GetFilenamesAdvice"); + named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice"); transformer.applyAdvice( named("getParts").and(takesArguments(1)), getClass().getName() + "$GetFilenamesFromMultiPartAdvice"); @@ -177,13 +176,12 @@ static void after( } /** - * Fires the {@code requestFilesFilenames} event when multipart content is parsed via the - * internal {@code getParts(MultiMap)} path triggered by {@code getParameter*()} / - * {@code getParameterMap()} — i.e. when the application never calls public {@code getParts()}. - * In Jetty 9.3+, {@code extractContentParameters()} assigns {@code _contentParameters} before - * calling this method, so {@code map == null} cannot be used as a "first parse" guard here; - * the call-depth guard prevents double-firing when {@code getParts()} internally delegates to - * this method. + * Fires the {@code requestFilesFilenames} event when multipart content is parsed via the internal + * {@code getParts(MultiMap)} path triggered by {@code getParameter*()} / {@code + * getParameterMap()} — i.e. when the application never calls public {@code getParts()}. In Jetty + * 9.3+, {@code extractContentParameters()} assigns {@code _contentParameters} before calling this + * method, so {@code map == null} cannot be used as a "first parse" guard here; the call-depth + * guard prevents double-firing when {@code getParts()} internally delegates to this method. */ @RequiresRequestContext(RequestContextSlot.APPSEC) public static class GetFilenamesFromMultiPartAdvice { From 0040e75f87f5900ff33214b87d8a9506eb6d7b5b Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 8 Apr 2026 12:13:52 +0200 Subject: [PATCH 07/12] Fix missing static imports for BODY_MULTIPART_REPEATED and BODY_MULTIPART_COMBINED --- .../groovy/datadog/trace/agent/test/base/HttpServerTest.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy index b7d07ea50d1..a599a987cbc 100644 --- a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy +++ b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy @@ -65,6 +65,8 @@ import java.util.function.Supplier import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_JSON import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART_COMBINED +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART_REPEATED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_URLENCODED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CREATED import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CREATED_IS From c5268ddf7be56ee101e58c61b5a040ef30e3e901 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 8 Apr 2026 12:42:55 +0200 Subject: [PATCH 08/12] Fix GetFilenamesAdvice double-fire for Jetty 9.4+ where _multiParts replaces _contentParameters as the getParts() cache In Jetty 9.3, getParts(MultiMap) sets _contentParameters, so the map==null guard prevents re-firing on repeated getParts() calls. In Jetty 9.4+, getParts() delegates to getParts(null) and caches the result in _multiParts instead, leaving _contentParameters null on every call. Add _multiParts==null as an additional guard (optional=true handles Jetty 9.3 where the field does not exist). --- ...uestExtractContentParametersInstrumentation.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java index 9404e0ad436..9edbe89ba41 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.function.BiFunction; import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.MultiMap; @@ -117,9 +118,17 @@ static void after( @RequiresRequestContext(RequestContextSlot.APPSEC) public static class GetFilenamesAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) - static boolean before(@Advice.FieldValue("_contentParameters") final MultiMap map) { + static boolean before( + @Advice.FieldValue("_contentParameters") final MultiMap contentParameters, + @Advice.FieldValue(value = "_multiParts", optional = true, typing = Assigner.Typing.DYNAMIC) + final Object multiParts) { final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Collection.class); - return callDepth == 0 && map == null; + // contentParameters is set by extractContentParameters() (called from getParameterMap()), + // so it being non-null means the request was already processed via that path. + // multiParts is set by getParts(MultiMap) (Jetty 9.4+) after the first getParts() call, + // so it being non-null means getParts() was already invoked and filenames were reported. + // In Jetty 9.3, _multiParts does not exist (optional=true → null). + return callDepth == 0 && contentParameters == null && multiParts == null; } @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) From db08e435bc0cdd7f1a5f20751110950fe274fb1e Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 8 Apr 2026 14:11:16 +0200 Subject: [PATCH 09/12] Fix GetFilenamesAdvice double-fire in jetty-appsec-8.1.3 In Jetty 8.x/9.0, _multiPartInputStream is null only on the first getParts() call. Add OnMethodEnter guard to skip the WAF callback on subsequent calls which return the cached multipart result. --- .../jetty8/RequestGetPartsInstrumentation.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java index 8937ead2e88..dba9ca9660b 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java @@ -34,6 +34,7 @@ import net.bytebuddy.description.method.MethodList; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.implementation.bytecode.assign.Assigner; import net.bytebuddy.jar.asm.ClassReader; import net.bytebuddy.jar.asm.ClassVisitor; import net.bytebuddy.jar.asm.ClassWriter; @@ -205,12 +206,22 @@ static void muzzle(Request req) throws ServletException, IOException { @RequiresRequestContext(RequestContextSlot.APPSEC) public static class GetFilenamesAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static boolean before( + @Advice.FieldValue(value = "_multiPartInputStream", typing = Assigner.Typing.DYNAMIC) + final Object multiPartInputStream) { + // _multiPartInputStream is null only on the first getParts() call; subsequent calls + // return the cached multipart result without re-parsing, but we must not re-fire the WAF. + return multiPartInputStream == null; + } + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) static void after( + @Advice.Enter boolean proceed, @Advice.Return Collection parts, @ActiveRequestContext RequestContext reqCtx, @Advice.Thrown(readOnly = false) Throwable t) { - if (t != null || parts == null || parts.isEmpty()) { + if (!proceed || t != null || parts == null || parts.isEmpty()) { return; } // Resolve getSubmittedFileName once (Servlet 3.1+; null on Servlet 3.0) From 30e4b8cfbf31c07dd2017ad36e076d7858ddf97b Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 8 Apr 2026 15:41:13 +0200 Subject: [PATCH 10/12] =?UTF-8?q?Fix=20GetFilenamesAdvice=20double-fire=20?= =?UTF-8?q?for=20all=20Jetty=209.3=E2=80=9311=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @Advice.FieldValue(optional=true) is not supported in ByteBuddy 1.11.22. Replace it with @Advice.This + inline reflection to detect whether getParts() has already been called on this request: - Jetty 9.4+: checks _multiParts (set after first getParts() call) - Jetty 9.3.x: falls back to _multiPartInputStream (the cache field in 9.3.x, where _multiParts does not exist and _contentParameters is only set by the getParameterMap() → extractContentParameters() path, not by getParts()) Covers all forkedTest and latestDepForkedTest suites for Jetty 9.0–11. --- ...tractContentParametersInstrumentation.java | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java index 9edbe89ba41..a102c1e9981 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/RequestExtractContentParametersInstrumentation.java @@ -24,7 +24,6 @@ import java.util.List; import java.util.function.BiFunction; import net.bytebuddy.asm.Advice; -import net.bytebuddy.implementation.bytecode.assign.Assigner; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.MultiMap; @@ -112,23 +111,49 @@ static void after( /** * Fires the {@code requestFilesFilenames} event when the application calls public {@code - * getParts()}. The {@code _contentParameters == null} guard ensures the WAF is invoked only on - * the first call — subsequent calls return the cached result without re-processing. + * getParts()}. Guards prevent double-firing: + * + *
    + *
  • {@code contentParameters != null}: set by {@code extractContentParameters()} (the {@code + * getParameterMap()} path); means filenames were already reported via {@code + * GetFilenamesFromMultiPartAdvice}. + *
  • {@code _multiParts != null} (Jetty 9.4+, read via reflection): set by the first {@code + * getParts()} call; means filenames were already reported. In Jetty 9.3 this field does not + * exist, so the reflection throws {@code NoSuchFieldException} and we treat it as null. + *
*/ @RequiresRequestContext(RequestContextSlot.APPSEC) public static class GetFilenamesAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) static boolean before( @Advice.FieldValue("_contentParameters") final MultiMap contentParameters, - @Advice.FieldValue(value = "_multiParts", optional = true, typing = Assigner.Typing.DYNAMIC) - final Object multiParts) { + @Advice.This final Request request) { final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Collection.class); - // contentParameters is set by extractContentParameters() (called from getParameterMap()), - // so it being non-null means the request was already processed via that path. - // multiParts is set by getParts(MultiMap) (Jetty 9.4+) after the first getParts() call, - // so it being non-null means getParts() was already invoked and filenames were reported. - // In Jetty 9.3, _multiParts does not exist (optional=true → null). - return callDepth == 0 && contentParameters == null && multiParts == null; + if (callDepth != 0 || contentParameters != null) { + return false; + } + // Check the multipart cache field to detect repeated calls. + // Jetty 9.4+: _multiParts is set after the first getParts() call. + // Jetty 9.3.x: _multiPartInputStream is set instead (_multiParts doesn't exist). + // A non-null value means getParts() was already invoked and filenames were reported. + try { + java.lang.reflect.Field f = request.getClass().getDeclaredField("_multiParts"); + f.setAccessible(true); + if (f.get(request) != null) { + return false; + } + } catch (NoSuchFieldException e9_3) { + try { + java.lang.reflect.Field f = request.getClass().getDeclaredField("_multiPartInputStream"); + f.setAccessible(true); + if (f.get(request) != null) { + return false; + } + } catch (Exception ignored) { + } + } catch (Exception ignored) { + } + return true; } @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) From 7f391b40ed6e9ad6a4f11ee6b4190ad7ab139395 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 9 Apr 2026 16:00:42 +0200 Subject: [PATCH 11/12] Simplify GetFilenamesAdvice in jetty-appsec-8.1.3: remove dead Servlet 3.1+ branch Jetty 8 implements only Servlet 3.0, so getSubmittedFileName() is never present on the Part objects. The reflection probe (try { getMethod("getSubmittedFileName") }) and the Servlet 3.1+ code path were dead code. Remove them and always parse filenames from the Content-Disposition header directly. --- .../RequestGetPartsInstrumentation.java | 49 ++++++------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java index dba9ca9660b..8b807904736 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java @@ -20,7 +20,6 @@ import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -224,41 +223,23 @@ static void after( if (!proceed || t != null || parts == null || parts.isEmpty()) { return; } - // Resolve getSubmittedFileName once (Servlet 3.1+; null on Servlet 3.0) - Method getSubmittedFileName = null; - try { - getSubmittedFileName = parts.iterator().next().getClass().getMethod("getSubmittedFileName"); - } catch (Exception ignored) { - } + // Jetty 8 implements Servlet 3.0; getSubmittedFileName does not exist. + // Parse filename from Content-Disposition header instead. List filenames = new ArrayList<>(); - if (getSubmittedFileName != null) { - // Servlet 3.1+: use getSubmittedFileName - for (Object part : parts) { - try { - String name = (String) getSubmittedFileName.invoke(part); - if (name != null && !name.isEmpty()) { - filenames.add(name); - } - } catch (Exception ignored) { - } - } - } else { - // Servlet 3.0: parse filename from Content-Disposition header - for (Object part : parts) { - String cd = ((Part) part).getHeader("content-disposition"); - if (cd != null) { - for (String tok : cd.split(";")) { - tok = tok.trim(); - if (tok.startsWith("filename=")) { - String name = tok.substring(9).trim(); - if (name.startsWith("\"") && name.endsWith("\"")) { - name = name.substring(1, name.length() - 1); - } - if (!name.isEmpty()) { - filenames.add(name); - } - break; + for (Object part : parts) { + String cd = ((Part) part).getHeader("content-disposition"); + if (cd != null) { + for (String tok : cd.split(";")) { + tok = tok.trim(); + if (tok.startsWith("filename=")) { + String name = tok.substring(9).trim(); + if (name.startsWith("\"") && name.endsWith("\"")) { + name = name.substring(1, name.length() - 1); + } + if (!name.isEmpty()) { + filenames.add(name); } + break; } } } From 246f4e31e8416c29cb8d820f78ac350e32c977c1 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 9 Apr 2026 16:16:34 +0200 Subject: [PATCH 12/12] Remove unnecessary casts in Jetty AppSec GetFilenamesAdvice Type @Advice.Return as Collection so the loop variable can be Part directly, eliminating the (Part) cast on each iteration. --- .../jetty8/RequestGetPartsInstrumentation.java | 6 +++--- .../RequestExtractContentParametersInstrumentation.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java index 8b807904736..75f45139624 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java @@ -217,7 +217,7 @@ static boolean before( @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) static void after( @Advice.Enter boolean proceed, - @Advice.Return Collection parts, + @Advice.Return Collection parts, @ActiveRequestContext RequestContext reqCtx, @Advice.Thrown(readOnly = false) Throwable t) { if (!proceed || t != null || parts == null || parts.isEmpty()) { @@ -226,8 +226,8 @@ static void after( // Jetty 8 implements Servlet 3.0; getSubmittedFileName does not exist. // Parse filename from Content-Disposition header instead. List filenames = new ArrayList<>(); - for (Object part : parts) { - String cd = ((Part) part).getHeader("content-disposition"); + for (Part part : parts) { + String cd = part.getHeader("content-disposition"); if (cd != null) { for (String tok : cd.split(";")) { tok = tok.trim(); diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java index 1835f6ffd0c..abddc58b323 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/RequestExtractContentParametersInstrumentation.java @@ -152,7 +152,7 @@ static boolean before(@Advice.FieldValue("_contentParameters") final MultiMap parts, @ActiveRequestContext RequestContext reqCtx, @Advice.Thrown(readOnly = false) Throwable t) { CallDepthThreadLocalMap.decrementCallDepth(Collection.class); @@ -160,8 +160,8 @@ static void after( return; } List filenames = new ArrayList<>(); - for (Object part : parts) { - String name = ((Part) part).getSubmittedFileName(); + for (Part part : parts) { + String name = part.getSubmittedFileName(); if (name != null && !name.isEmpty()) { filenames.add(name); }