From 629f0742b04535ddaf052f9d3c4da3ec000ce411 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Mon, 23 Mar 2026 14:03:57 +0100 Subject: [PATCH 01/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] =?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/18] 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/18] 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); } From 20f8bb4dc190380f183d30cc1ab007e04cbc73ef Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 9 Apr 2026 16:44:09 +0200 Subject: [PATCH 13/18] Extract Content-Disposition parsing to MultipartHelper + unit tests Move the filename extraction logic from GetFilenamesAdvice into a new MultipartHelper helper class so it can be unit tested in isolation. Add 12 Spock test cases covering quoted/unquoted filenames, empty values, whitespace, null input, and edge cases. --- .../jetty8/MultipartHelper.java | 38 +++++++++++++++++++ .../RequestGetPartsInstrumentation.java | 20 +++------- .../jetty8/MultipartHelperTest.groovy | 28 ++++++++++++++ 3 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/MultipartHelper.java create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/test/groovy/datadog/trace/instrumentation/jetty8/MultipartHelperTest.groovy diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/MultipartHelper.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/MultipartHelper.java new file mode 100644 index 00000000000..28426fd9667 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/MultipartHelper.java @@ -0,0 +1,38 @@ +package datadog.trace.instrumentation.jetty8; + +public class MultipartHelper { + + private MultipartHelper() {} + + /** + * Extracts the filename value from a {@code Content-Disposition} header string. + * + *

Jetty 8 implements Servlet 3.0, where {@code Part.getSubmittedFileName()} does not exist. + * This method parses the filename from the header manually. + * + *

Examples of handled inputs: + * + *

+   *   form-data; name="file"; filename="photo.jpg"  → "photo.jpg"
+   *   form-data; name="file"; filename=photo.jpg    → "photo.jpg"
+   * 
+ * + * @return the filename, or {@code null} if not present or empty + */ + public static String filenameFromContentDisposition(String cd) { + if (cd == null) { + return 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); + } + return name.isEmpty() ? null : name; + } + } + return null; + } +} 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 75f45139624..515f988e5bd 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 @@ -65,6 +65,7 @@ public String[] helperClassNames() { packageName + ".ParameterCollector", packageName + ".ParameterCollector$ParameterCollectorImpl", packageName + ".ParameterCollector$ParameterCollectorNoop", + packageName + ".MultipartHelper", }; } @@ -227,21 +228,10 @@ static void after( // Parse filename from Content-Disposition header instead. List filenames = new ArrayList<>(); for (Part part : parts) { - String cd = 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; - } - } + String name = + MultipartHelper.filenameFromContentDisposition(part.getHeader("content-disposition")); + if (name != null) { + filenames.add(name); } } if (filenames.isEmpty()) { diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/test/groovy/datadog/trace/instrumentation/jetty8/MultipartHelperTest.groovy b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/test/groovy/datadog/trace/instrumentation/jetty8/MultipartHelperTest.groovy new file mode 100644 index 00000000000..46d96465146 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/test/groovy/datadog/trace/instrumentation/jetty8/MultipartHelperTest.groovy @@ -0,0 +1,28 @@ +package datadog.trace.instrumentation.jetty8 + +import spock.lang.Specification +import spock.lang.Unroll + +class MultipartHelperTest extends Specification { + + @Unroll + def "filenameFromContentDisposition: #description"() { + expect: + MultipartHelper.filenameFromContentDisposition(cd) == expected + + where: + description | cd | expected + 'null input' | null | null + 'no filename token' | 'form-data; name="upload"' | null + 'quoted filename' | 'form-data; name="upload"; filename="photo.jpg"' | 'photo.jpg' + 'unquoted filename' | 'form-data; name="upload"; filename=photo.jpg' | 'photo.jpg' + 'filename with spaces' | 'form-data; filename="my file.jpg"' | 'my file.jpg' + 'empty quoted filename' | 'form-data; name="f"; filename=""' | null + 'empty unquoted filename' | 'form-data; name="f"; filename=' | null + 'whitespace-only unquoted filename' | 'form-data; name="f"; filename= ' | null + 'filename first token' | 'filename="evil.php"' | 'evil.php' + 'extra spaces around semicolons' | 'form-data ; name="f" ; filename="a.txt" ' | 'a.txt' + 'filename with dots and dashes' | 'form-data; filename="my-file.v2.tar.gz"' | 'my-file.v2.tar.gz' + 'stops at first filename= token' | 'form-data; filename="first.jpg"; filename="second.jpg"' | 'first.jpg' + } +} From 398ea51fae5e19602db14e94fbee4327734c075e Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 9 Apr 2026 17:04:28 +0200 Subject: [PATCH 14/18] Extract filename extraction to MultipartHelper in jetty-appsec-9.2 + unit tests Move the getSubmittedFileName() loop from GetFilenamesAdvice into a new MultipartHelper helper class (injected via helperClassNames) so it can be unit tested in isolation. Add 8 Spock test cases covering null/empty collections, null/empty filenames, multiple parts, and special characters. --- .../jetty92/MultipartHelper.java | 32 +++++++++ ...tractContentParametersInstrumentation.java | 14 ++-- .../jetty92/MultipartHelperTest.groovy | 71 +++++++++++++++++++ 3 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/MultipartHelper.java create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/test/groovy/datadog/trace/instrumentation/jetty92/MultipartHelperTest.groovy diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/MultipartHelper.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/MultipartHelper.java new file mode 100644 index 00000000000..714a6bd5339 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/main/java/datadog/trace/instrumentation/jetty92/MultipartHelper.java @@ -0,0 +1,32 @@ +package datadog.trace.instrumentation.jetty92; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.servlet.http.Part; + +public class MultipartHelper { + + private MultipartHelper() {} + + /** + * Extracts non-null, non-empty filenames from a collection of multipart {@link Part}s using + * {@link Part#getSubmittedFileName()} (Servlet 3.1+). + * + * @return list of filenames; never {@code null}, may be empty + */ + public static List extractFilenames(Collection parts) { + if (parts == null || parts.isEmpty()) { + return Collections.emptyList(); + } + List filenames = new ArrayList<>(); + for (Part part : parts) { + String name = part.getSubmittedFileName(); + if (name != null && !name.isEmpty()) { + filenames.add(name); + } + } + return filenames; + } +} 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 abddc58b323..f43bfd88143 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,6 @@ 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; @@ -42,6 +41,11 @@ public String instrumentedType() { return "org.eclipse.jetty.server.Request"; } + @Override + public String[] helperClassNames() { + return new String[] {packageName + ".MultipartHelper"}; + } + @Override public void methodAdvice(MethodTransformer transformer) { transformer.applyAdvice( @@ -159,13 +163,7 @@ static void after( if (!proceed || t != null || parts == null || parts.isEmpty()) { return; } - List filenames = new ArrayList<>(); - for (Part part : parts) { - String name = part.getSubmittedFileName(); - if (name != null && !name.isEmpty()) { - filenames.add(name); - } - } + List filenames = MultipartHelper.extractFilenames(parts); if (filenames.isEmpty()) { return; } diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/test/groovy/datadog/trace/instrumentation/jetty92/MultipartHelperTest.groovy b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/test/groovy/datadog/trace/instrumentation/jetty92/MultipartHelperTest.groovy new file mode 100644 index 00000000000..51bdb50c564 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.2/src/test/groovy/datadog/trace/instrumentation/jetty92/MultipartHelperTest.groovy @@ -0,0 +1,71 @@ +package datadog.trace.instrumentation.jetty92 + +import javax.servlet.http.Part +import spock.lang.Specification + +class MultipartHelperTest extends Specification { + + def "returns empty list for null collection"() { + expect: + MultipartHelper.extractFilenames(null) == [] + } + + def "returns empty list for empty collection"() { + expect: + MultipartHelper.extractFilenames([]) == [] + } + + def "returns empty list when all parts have null filename"() { + given: + def parts = [part(null), part(null)] + + expect: + MultipartHelper.extractFilenames(parts) == [] + } + + def "returns empty list when all parts have empty filename"() { + given: + def parts = [part(''), part('')] + + expect: + MultipartHelper.extractFilenames(parts) == [] + } + + def "extracts filename from single part"() { + given: + def parts = [part('photo.jpg')] + + expect: + MultipartHelper.extractFilenames(parts) == ['photo.jpg'] + } + + def "extracts filenames from multiple parts"() { + given: + def parts = [part('a.jpg'), part('b.png'), part('c.pdf')] + + expect: + MultipartHelper.extractFilenames(parts) == ['a.jpg', 'b.png', 'c.pdf'] + } + + def "skips parts with null or empty filename and keeps valid ones"() { + given: + def parts = [part(null), part('valid.txt'), part(''), part('other.zip')] + + expect: + MultipartHelper.extractFilenames(parts) == ['valid.txt', 'other.zip'] + } + + def "preserves filenames with spaces and special characters"() { + given: + def parts = [part('my file.tar.gz'), part('résumé.pdf')] + + expect: + MultipartHelper.extractFilenames(parts) == ['my file.tar.gz', 'résumé.pdf'] + } + + private Part part(String submittedFileName) { + Part p = Stub(Part) + p.getSubmittedFileName() >> submittedFileName + return p + } +} From 52c5cb8d892df615b29d82b8ba7943d4badb01ff Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Fri, 10 Apr 2026 09:49:53 +0200 Subject: [PATCH 15/18] Split jetty-appsec-9.3 [9.3,12) into three clean modules: 9.3, 9.4, 11.0 Eliminates all reflection from the multipart filename instrumentation by creating version-specific modules with compile-time type safety: - jetty-appsec-9.3 [9.3,9.4): javax.servlet, uses _multiPartInputStream field - jetty-appsec-9.4 [9.4,11.0): javax.servlet, uses _multiParts field - jetty-appsec-11.0 [11.0,12.0): jakarta.servlet, uses _multiParts field Each module uses muzzle references as version discriminators instead of runtime reflection, and delegates filename extraction to a testable MultipartHelper class with 8 Spock unit tests each. Server test modules updated to reference the correct appsec module per Jetty version range. --- .../jetty-appsec-11.0/build.gradle | 24 ++ .../jetty-appsec-11.0/gradle.lockfile | 127 ++++++++++ .../jetty11/MultipartHelper.java | 32 +++ ...tractContentParametersInstrumentation.java | 231 ++++++++++++++++++ .../jetty11/MultipartHelperTest.groovy | 71 ++++++ .../jetty-appsec-9.3/build.gradle | 2 +- .../jetty93/MultipartHelper.java | 32 +++ ...tractContentParametersInstrumentation.java | 95 ++----- .../jetty93/MultipartHelperTest.groovy | 71 ++++++ .../jetty-appsec-9.4/build.gradle | 16 ++ .../jetty-appsec-9.4/gradle.lockfile | 126 ++++++++++ .../jetty94/MultipartHelper.java | 32 +++ ...tractContentParametersInstrumentation.java | 231 ++++++++++++++++++ .../jetty94/MultipartHelperTest.groovy | 71 ++++++ .../jetty-server-10.0/build.gradle | 6 +- .../jetty-server-11.0/build.gradle | 2 +- .../jetty-server-12.0/build.gradle | 2 + .../jetty-server-9.3/build.gradle | 1 + .../jetty-server-9.4.21/build.gradle | 2 +- settings.gradle.kts | 2 + 20 files changed, 1100 insertions(+), 76 deletions(-) create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/build.gradle create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/gradle.lockfile create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/main/java/datadog/trace/instrumentation/jetty11/MultipartHelper.java create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/main/java/datadog/trace/instrumentation/jetty11/RequestExtractContentParametersInstrumentation.java create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/test/groovy/datadog/trace/instrumentation/jetty11/MultipartHelperTest.groovy create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/MultipartHelper.java create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/test/groovy/datadog/trace/instrumentation/jetty93/MultipartHelperTest.groovy create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/build.gradle create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/gradle.lockfile create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/MultipartHelper.java create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/RequestExtractContentParametersInstrumentation.java create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/test/groovy/datadog/trace/instrumentation/jetty94/MultipartHelperTest.groovy diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/build.gradle new file mode 100644 index 00000000000..718d98bd326 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/build.gradle @@ -0,0 +1,24 @@ +muzzle { + pass { + group = 'org.eclipse.jetty' + module = 'jetty-server' + versions = '[11.0,12.0)' + assertInverse = true + javaVersion = 11 + } +} + +apply from: "$rootDir/gradle/java.gradle" + +dependencies { + compileOnly(group: 'org.eclipse.jetty', name: 'jetty-server', version: '11.0.0') { + exclude group: 'org.slf4j', module: 'slf4j-api' + } + testImplementation(group: 'org.eclipse.jetty.toolchain', name: 'jetty-jakarta-servlet-api', version: '5.0.1') +} + +tasks.withType(JavaCompile).configureEach { + configureCompiler(it, 11, JavaVersion.VERSION_1_8) +} + +// testing happens in the jetty-* modules diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/gradle.lockfile b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/gradle.lockfile new file mode 100644 index 00000000000..ddb930c8200 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/gradle.lockfile @@ -0,0 +1,127 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +cafe.cryptography:curve25519-elisabeth:0.1.0=testRuntimeClasspath +cafe.cryptography:ed25519-elisabeth:0.1.0=testRuntimeClasspath +ch.qos.logback:logback-classic:1.2.13=testCompileClasspath,testRuntimeClasspath +ch.qos.logback:logback-core:1.2.13=testCompileClasspath,testRuntimeClasspath +com.blogspot.mydailyjava:weak-lock-free:0.17=buildTimeInstrumentationPlugin,compileClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq.okhttp3:okhttp:3.12.15=testCompileClasspath,testRuntimeClasspath +com.datadoghq.okio:okio:1.17.6=testCompileClasspath,testRuntimeClasspath +com.datadoghq:dd-instrument-java:0.0.3=buildTimeInstrumentationPlugin,compileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq:dd-javac-plugin-client:0.2.2=buildTimeInstrumentationPlugin,compileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq:java-dogstatsd-client:4.4.3=testRuntimeClasspath +com.datadoghq:sketches-java:0.8.3=testRuntimeClasspath +com.github.javaparser:javaparser-core:3.25.6=codenarc +com.github.jnr:jffi:1.3.14=testRuntimeClasspath +com.github.jnr:jnr-a64asm:1.0.0=testRuntimeClasspath +com.github.jnr:jnr-constants:0.10.4=testRuntimeClasspath +com.github.jnr:jnr-enxio:0.32.19=testRuntimeClasspath +com.github.jnr:jnr-ffi:2.2.18=testRuntimeClasspath +com.github.jnr:jnr-posix:3.1.21=testRuntimeClasspath +com.github.jnr:jnr-unixsocket:0.38.24=testRuntimeClasspath +com.github.jnr:jnr-x86asm:1.0.2=testRuntimeClasspath +com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath +com.github.spotbugs:spotbugs:4.9.8=spotbugs +com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs +com.google.auto.service:auto-service-annotations:1.1.1=annotationProcessor,compileClasspath,testAnnotationProcessor,testCompileClasspath +com.google.auto.service:auto-service:1.1.1=annotationProcessor,testAnnotationProcessor +com.google.auto:auto-common:1.2.1=annotationProcessor,testAnnotationProcessor +com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,compileClasspath,spotbugs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.13.2=spotbugs +com.google.errorprone:error_prone_annotations:2.18.0=annotationProcessor,testAnnotationProcessor +com.google.errorprone:error_prone_annotations:2.41.0=spotbugs +com.google.guava:failureaccess:1.0.1=annotationProcessor,testAnnotationProcessor +com.google.guava:guava:20.0=testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.0.1-jre=annotationProcessor,testAnnotationProcessor +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,testAnnotationProcessor +com.google.j2objc:j2objc-annotations:2.8=annotationProcessor,testAnnotationProcessor +com.google.re2j:re2j:1.7=testRuntimeClasspath +com.squareup.moshi:moshi:1.11.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:logging-interceptor:3.12.12=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:3.12.12=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:1.17.5=testCompileClasspath,testRuntimeClasspath +com.thoughtworks.qdox:qdox:1.12.1=codenarc +commons-fileupload:commons-fileupload:1.5=testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.11.0=testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.20.0=spotbugs +de.thetaphi:forbiddenapis:3.10=compileClasspath +io.leangen.geantyref:geantyref:1.3.16=testRuntimeClasspath +io.sqreen:libsqreen:17.3.0=testRuntimeClasspath +javax.servlet:javax.servlet-api:3.1.0=testCompileClasspath,testRuntimeClasspath +jaxen:jaxen:2.0.0=spotbugs +junit:junit:4.13.2=testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.18.3=buildTimeInstrumentationPlugin,compileClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.3=buildTimeInstrumentationPlugin,compileClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna-platform:5.8.0=testRuntimeClasspath +net.java.dev.jna:jna:5.8.0=testRuntimeClasspath +net.sf.saxon:Saxon-HE:12.9=spotbugs +org.apache.ant:ant-antlr:1.10.14=codenarc +org.apache.ant:ant-junit:1.10.14=codenarc +org.apache.bcel:bcel:6.11.0=spotbugs +org.apache.commons:commons-lang3:3.19.0=spotbugs +org.apache.commons:commons-text:1.14.0=spotbugs +org.apache.logging.log4j:log4j-api:2.25.2=spotbugs +org.apache.logging.log4j:log4j-core:2.25.2=spotbugs +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=annotationProcessor,testAnnotationProcessor +org.codehaus.groovy:groovy-ant:3.0.23=codenarc +org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc +org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc +org.codehaus.groovy:groovy-json:3.0.23=codenarc +org.codehaus.groovy:groovy-json:3.0.25=testCompileClasspath,testRuntimeClasspath +org.codehaus.groovy:groovy-templates:3.0.23=codenarc +org.codehaus.groovy:groovy-xml:3.0.23=codenarc +org.codehaus.groovy:groovy:3.0.23=codenarc +org.codehaus.groovy:groovy:3.0.25=testCompileClasspath,testRuntimeClasspath +org.codenarc:CodeNarc:3.7.0=codenarc +org.dom4j:dom4j:2.2.0=spotbugs +org.eclipse.jetty.toolchain:jetty-jakarta-servlet-api:5.0.1=compileClasspath,testCompileClasspath,testRuntimeClasspath +org.eclipse.jetty:jetty-http:11.0.0=compileClasspath +org.eclipse.jetty:jetty-io:11.0.0=compileClasspath +org.eclipse.jetty:jetty-server:11.0.0=compileClasspath +org.eclipse.jetty:jetty-util:11.0.0=compileClasspath +org.gmetrics:GMetrics:2.1.0=codenarc +org.hamcrest:hamcrest-core:1.3=testRuntimeClasspath +org.hamcrest:hamcrest:3.0=testCompileClasspath,testRuntimeClasspath +org.jctools:jctools-core-jdk11:4.0.6=testRuntimeClasspath +org.jctools:jctools-core:4.0.6=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.14.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.14.1=testRuntimeClasspath +org.junit.platform:junit-platform-runner:1.14.1=testRuntimeClasspath +org.junit.platform:junit-platform-suite-api:1.14.1=testRuntimeClasspath +org.junit.platform:junit-platform-suite-commons:1.14.1=testRuntimeClasspath +org.junit:junit-bom:5.14.0=spotbugs +org.junit:junit-bom:5.14.1=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-core:4.4.0=testRuntimeClasspath +org.objenesis:objenesis:3.3=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.ow2.asm:asm-analysis:9.7.1=testRuntimeClasspath +org.ow2.asm:asm-analysis:9.9=spotbugs +org.ow2.asm:asm-commons:9.9=spotbugs +org.ow2.asm:asm-commons:9.9.1=testRuntimeClasspath +org.ow2.asm:asm-tree:9.9=spotbugs +org.ow2.asm:asm-tree:9.9.1=testRuntimeClasspath +org.ow2.asm:asm-util:9.7.1=testRuntimeClasspath +org.ow2.asm:asm-util:9.9=spotbugs +org.ow2.asm:asm:9.9=spotbugs +org.ow2.asm:asm:9.9.1=buildTimeInstrumentationPlugin,compileClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:jcl-over-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath +org.slf4j:jul-to-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath +org.slf4j:log4j-over-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:1.7.30=buildTimeInstrumentationPlugin,compileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath +org.slf4j:slf4j-api:1.7.32=testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.17=spotbugs,spotbugsSlf4j +org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j +org.snakeyaml:snakeyaml-engine:2.9=buildTimeInstrumentationPlugin,muzzleTooling,runtimeClasspath,testRuntimeClasspath +org.spockframework:spock-bom:2.4-groovy-3.0=testCompileClasspath,testRuntimeClasspath +org.spockframework:spock-core:2.4-groovy-3.0=testCompileClasspath,testRuntimeClasspath +org.tabletest:tabletest-junit:1.2.1=testCompileClasspath,testRuntimeClasspath +org.tabletest:tabletest-parser:1.2.0=testCompileClasspath,testRuntimeClasspath +org.xmlresolver:xmlresolver:5.3.3=spotbugs +empty=spotbugsPlugins diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/main/java/datadog/trace/instrumentation/jetty11/MultipartHelper.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/main/java/datadog/trace/instrumentation/jetty11/MultipartHelper.java new file mode 100644 index 00000000000..dc053ed19b8 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/main/java/datadog/trace/instrumentation/jetty11/MultipartHelper.java @@ -0,0 +1,32 @@ +package datadog.trace.instrumentation.jetty11; + +import jakarta.servlet.http.Part; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class MultipartHelper { + + private MultipartHelper() {} + + /** + * Extracts non-null, non-empty filenames from a collection of multipart {@link Part}s using + * {@link Part#getSubmittedFileName()} (Servlet 5.0+, Jetty 11.x). + * + * @return list of filenames; never {@code null}, may be empty + */ + public static List extractFilenames(Collection parts) { + if (parts == null || parts.isEmpty()) { + return Collections.emptyList(); + } + List filenames = new ArrayList<>(); + for (Part part : parts) { + String name = part.getSubmittedFileName(); + if (name != null && !name.isEmpty()) { + filenames.add(name); + } + } + return filenames; + } +} diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/main/java/datadog/trace/instrumentation/jetty11/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/main/java/datadog/trace/instrumentation/jetty11/RequestExtractContentParametersInstrumentation.java new file mode 100644 index 00000000000..0a68d9d48c1 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/main/java/datadog/trace/instrumentation/jetty11/RequestExtractContentParametersInstrumentation.java @@ -0,0 +1,231 @@ +package datadog.trace.instrumentation.jetty11; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.api.gateway.Events.EVENTS; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +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.agent.tooling.muzzle.Reference; +import datadog.trace.api.gateway.BlockResponseFunction; +import datadog.trace.api.gateway.CallbackProvider; +import datadog.trace.api.gateway.Flow; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import jakarta.servlet.http.Part; +import java.util.Collection; +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; + +@AutoService(InstrumenterModule.class) +public class RequestExtractContentParametersInstrumentation extends InstrumenterModule.AppSec + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + private static final String MULTI_MAP_INTERNAL_NAME = "Lorg/eclipse/jetty/util/MultiMap;"; + + public RequestExtractContentParametersInstrumentation() { + super("jetty"); + } + + @Override + public String instrumentedType() { + return "org.eclipse.jetty.server.Request"; + } + + @Override + public String[] helperClassNames() { + return new String[] {packageName + ".MultipartHelper"}; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("extractContentParameters").and(takesArguments(0)).or(named("getParts")), + getClass().getName() + "$ExtractContentParametersAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(1)), + getClass().getName() + "$GetFilenamesFromMultiPartAdvice"); + } + + // Discriminates Jetty 11.x ([11.0, 12.0)): + // - _contentParameters + extractContentParameters(void) exist in 11.x (excludes Jetty 12) + // - _multiParts exists in 11.x + // - jakarta.servlet.http.Part exists in 11.x classpath (excludes 9.4–10.x which use javax) + private static final Reference REQUEST_REFERENCE = + new Reference.Builder("org.eclipse.jetty.server.Request") + .withMethod(new String[0], 0, "extractContentParameters", "V") + .withField(new String[0], 0, "_contentParameters", MULTI_MAP_INTERNAL_NAME) + .withField(new String[0], 0, "_multiParts", "Lorg/eclipse/jetty/server/MultiParts;") + .build(); + + private static final Reference JAKARTA_PART_REFERENCE = + new Reference.Builder("jakarta.servlet.http.Part").build(); + + @Override + public Reference[] additionalMuzzleReferences() { + return new Reference[] {REQUEST_REFERENCE, JAKARTA_PART_REFERENCE}; + } + + @RequiresRequestContext(RequestContextSlot.APPSEC) + public static class ExtractContentParametersAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static boolean before(@Advice.FieldValue("_contentParameters") final MultiMap map) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Request.class); + return callDepth == 0 && map == null; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Enter boolean proceed, + @Advice.FieldValue("_contentParameters") final MultiMap map, + @ActiveRequestContext RequestContext reqCtx, + @Advice.Thrown(readOnly = false) Throwable t) { + CallDepthThreadLocalMap.decrementCallDepth(Request.class); + if (!proceed) { + return; + } + if (map == null || map.isEmpty()) { + return; + } + + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction> callback = + cbp.getCallback(EVENTS.requestBodyProcessed()); + if (callback == null) { + return; + } + + Flow flow = callback.apply(reqCtx, map); + Flow.Action action = flow.getAction(); + if (action instanceof Flow.Action.RequestBlockingAction) { + Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action; + BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction(); + if (blockResponseFunction != null) { + blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba); + if (t == null) { + t = new BlockingException("Blocked request (for Request/extractContentParameters)"); + reqCtx.getTraceSegment().effectivelyBlocked(); + } + } + } + } + } + + /** + * Fires the {@code requestFilesFilenames} event when the application calls public {@code + * 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}: set by the first {@code getParts()} call in Jetty 11.x; + * means filenames were already reported. + *
+ */ + @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", typing = Assigner.Typing.DYNAMIC) + final Object multiParts) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Collection.class); + return callDepth == 0 && contentParameters == null && multiParts == 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) { + CallDepthThreadLocalMap.decrementCallDepth(Collection.class); + if (!proceed || t != null || parts == null || parts.isEmpty()) { + return; + } + List filenames = MultipartHelper.extractFilenames(parts); + 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(); + } + } + } + } + } + + /** + * 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()}. + */ + @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; + } + List filenames = MultipartHelper.extractFilenames(parts); + 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-11.0/src/test/groovy/datadog/trace/instrumentation/jetty11/MultipartHelperTest.groovy b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/test/groovy/datadog/trace/instrumentation/jetty11/MultipartHelperTest.groovy new file mode 100644 index 00000000000..2434cc72fae --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/test/groovy/datadog/trace/instrumentation/jetty11/MultipartHelperTest.groovy @@ -0,0 +1,71 @@ +package datadog.trace.instrumentation.jetty11 + +import jakarta.servlet.http.Part +import spock.lang.Specification + +class MultipartHelperTest extends Specification { + + def "returns empty list for null collection"() { + expect: + MultipartHelper.extractFilenames(null) == [] + } + + def "returns empty list for empty collection"() { + expect: + MultipartHelper.extractFilenames([]) == [] + } + + def "returns empty list when all parts have null filename"() { + given: + def parts = [part(null), part(null)] + + expect: + MultipartHelper.extractFilenames(parts) == [] + } + + def "returns empty list when all parts have empty filename"() { + given: + def parts = [part(''), part('')] + + expect: + MultipartHelper.extractFilenames(parts) == [] + } + + def "extracts filename from single part"() { + given: + def parts = [part('photo.jpg')] + + expect: + MultipartHelper.extractFilenames(parts) == ['photo.jpg'] + } + + def "extracts filenames from multiple parts"() { + given: + def parts = [part('a.jpg'), part('b.png'), part('c.pdf')] + + expect: + MultipartHelper.extractFilenames(parts) == ['a.jpg', 'b.png', 'c.pdf'] + } + + def "skips parts with null or empty filename and keeps valid ones"() { + given: + def parts = [part(null), part('valid.txt'), part(''), part('other.zip')] + + expect: + MultipartHelper.extractFilenames(parts) == ['valid.txt', 'other.zip'] + } + + def "preserves filenames with spaces and special characters"() { + given: + def parts = [part('my file.tar.gz'), part('résumé.pdf')] + + expect: + MultipartHelper.extractFilenames(parts) == ['my file.tar.gz', 'résumé.pdf'] + } + + private Part part(String submittedFileName) { + Part p = Stub(Part) + p.getSubmittedFileName() >> submittedFileName + return p + } +} diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/build.gradle index 69bad38c12b..d32eda9a3eb 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/build.gradle @@ -2,7 +2,7 @@ muzzle { pass { group = 'org.eclipse.jetty' module = 'jetty-server' - versions = '[9.3,12)' + versions = '[9.3,9.4)' assertInverse = true } } diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/MultipartHelper.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/MultipartHelper.java new file mode 100644 index 00000000000..e54f72b8ff7 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/main/java/datadog/trace/instrumentation/jetty93/MultipartHelper.java @@ -0,0 +1,32 @@ +package datadog.trace.instrumentation.jetty93; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.servlet.http.Part; + +public class MultipartHelper { + + private MultipartHelper() {} + + /** + * Extracts non-null, non-empty filenames from a collection of multipart {@link Part}s using + * {@link Part#getSubmittedFileName()} (Servlet 3.1+, Jetty 9.3.x). + * + * @return list of filenames; never {@code null}, may be empty + */ + public static List extractFilenames(Collection parts) { + if (parts == null || parts.isEmpty()) { + return Collections.emptyList(); + } + List filenames = new ArrayList<>(); + for (Part part : parts) { + String name = part.getSubmittedFileName(); + if (name != null && !name.isEmpty()) { + filenames.add(name); + } + } + return filenames; + } +} 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 a102c1e9981..5ecd1d67435 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,12 +18,12 @@ 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 javax.servlet.http.Part; import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.MultiMap; @@ -41,6 +41,11 @@ public String instrumentedType() { return "org.eclipse.jetty.server.Request"; } + @Override + public String[] helperClassNames() { + return new String[] {packageName + ".MultipartHelper"}; + } + @Override public void methodAdvice(MethodTransformer transformer) { transformer.applyAdvice( @@ -53,10 +58,18 @@ public void methodAdvice(MethodTransformer transformer) { getClass().getName() + "$GetFilenamesFromMultiPartAdvice"); } + // Discriminates Jetty 9.3.x ([9.3, 9.4)): + // - _contentParameters + extractContentParameters(void) exist from 9.3+ (excludes 9.2) + // - _multiPartInputStream exists only in 9.3.x (excludes 9.4+ where it became _multiParts) private static final Reference REQUEST_REFERENCE = new Reference.Builder("org.eclipse.jetty.server.Request") .withMethod(new String[0], 0, "extractContentParameters", "V") .withField(new String[0], 0, "_contentParameters", MULTI_MAP_INTERNAL_NAME) + .withField( + new String[0], + 0, + "_multiPartInputStream", + "Lorg/eclipse/jetty/util/MultiPartInputStreamParser;") .build(); @Override @@ -114,12 +127,11 @@ static void after( * getParts()}. Guards prevent double-firing: * *
    - *
  • {@code contentParameters != null}: set by {@code extractContentParameters()} (the {@code + *
  • {@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. + *
  • {@code _multiPartInputStream != null}: set by the first {@code getParts()} call in Jetty + * 9.3.x; means filenames were already reported. *
*/ @RequiresRequestContext(RequestContextSlot.APPSEC) @@ -127,63 +139,23 @@ public static class GetFilenamesAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) static boolean before( @Advice.FieldValue("_contentParameters") final MultiMap contentParameters, - @Advice.This final Request request) { + @Advice.FieldValue(value = "_multiPartInputStream", typing = Assigner.Typing.DYNAMIC) + final Object multiPartInputStream) { final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Collection.class); - 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; + return callDepth == 0 && contentParameters == null && multiPartInputStream == null; } @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) { 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) { - } - } + List filenames = MultipartHelper.extractFilenames(parts); if (filenames.isEmpty()) { return; } @@ -227,31 +199,14 @@ 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) { 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) { - } - } + List filenames = MultipartHelper.extractFilenames(parts); if (filenames.isEmpty()) { return; } diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/test/groovy/datadog/trace/instrumentation/jetty93/MultipartHelperTest.groovy b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/test/groovy/datadog/trace/instrumentation/jetty93/MultipartHelperTest.groovy new file mode 100644 index 00000000000..31489cb96f9 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/src/test/groovy/datadog/trace/instrumentation/jetty93/MultipartHelperTest.groovy @@ -0,0 +1,71 @@ +package datadog.trace.instrumentation.jetty93 + +import javax.servlet.http.Part +import spock.lang.Specification + +class MultipartHelperTest extends Specification { + + def "returns empty list for null collection"() { + expect: + MultipartHelper.extractFilenames(null) == [] + } + + def "returns empty list for empty collection"() { + expect: + MultipartHelper.extractFilenames([]) == [] + } + + def "returns empty list when all parts have null filename"() { + given: + def parts = [part(null), part(null)] + + expect: + MultipartHelper.extractFilenames(parts) == [] + } + + def "returns empty list when all parts have empty filename"() { + given: + def parts = [part(''), part('')] + + expect: + MultipartHelper.extractFilenames(parts) == [] + } + + def "extracts filename from single part"() { + given: + def parts = [part('photo.jpg')] + + expect: + MultipartHelper.extractFilenames(parts) == ['photo.jpg'] + } + + def "extracts filenames from multiple parts"() { + given: + def parts = [part('a.jpg'), part('b.png'), part('c.pdf')] + + expect: + MultipartHelper.extractFilenames(parts) == ['a.jpg', 'b.png', 'c.pdf'] + } + + def "skips parts with null or empty filename and keeps valid ones"() { + given: + def parts = [part(null), part('valid.txt'), part(''), part('other.zip')] + + expect: + MultipartHelper.extractFilenames(parts) == ['valid.txt', 'other.zip'] + } + + def "preserves filenames with spaces and special characters"() { + given: + def parts = [part('my file.tar.gz'), part('résumé.pdf')] + + expect: + MultipartHelper.extractFilenames(parts) == ['my file.tar.gz', 'résumé.pdf'] + } + + private Part part(String submittedFileName) { + Part p = Stub(Part) + p.getSubmittedFileName() >> submittedFileName + return p + } +} diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/build.gradle new file mode 100644 index 00000000000..d2f68aec403 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/build.gradle @@ -0,0 +1,16 @@ +muzzle { + pass { + group = 'org.eclipse.jetty' + module = 'jetty-server' + versions = '[9.4,11.0)' + assertInverse = true + } +} + +apply from: "$rootDir/gradle/java.gradle" + +dependencies { + compileOnly group: 'org.eclipse.jetty', name: 'jetty-server', version: '9.4.21.v20190926' +} + +// testing happens in the jetty-* modules diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/gradle.lockfile b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/gradle.lockfile new file mode 100644 index 00000000000..961940d455a --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/gradle.lockfile @@ -0,0 +1,126 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +cafe.cryptography:curve25519-elisabeth:0.1.0=testRuntimeClasspath +cafe.cryptography:ed25519-elisabeth:0.1.0=testRuntimeClasspath +ch.qos.logback:logback-classic:1.2.13=testCompileClasspath,testRuntimeClasspath +ch.qos.logback:logback-core:1.2.13=testCompileClasspath,testRuntimeClasspath +com.blogspot.mydailyjava:weak-lock-free:0.17=buildTimeInstrumentationPlugin,compileClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq.okhttp3:okhttp:3.12.15=testCompileClasspath,testRuntimeClasspath +com.datadoghq.okio:okio:1.17.6=testCompileClasspath,testRuntimeClasspath +com.datadoghq:dd-instrument-java:0.0.3=buildTimeInstrumentationPlugin,compileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq:dd-javac-plugin-client:0.2.2=buildTimeInstrumentationPlugin,compileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq:java-dogstatsd-client:4.4.3=testRuntimeClasspath +com.datadoghq:sketches-java:0.8.3=testRuntimeClasspath +com.github.javaparser:javaparser-core:3.25.6=codenarc +com.github.jnr:jffi:1.3.14=testRuntimeClasspath +com.github.jnr:jnr-a64asm:1.0.0=testRuntimeClasspath +com.github.jnr:jnr-constants:0.10.4=testRuntimeClasspath +com.github.jnr:jnr-enxio:0.32.19=testRuntimeClasspath +com.github.jnr:jnr-ffi:2.2.18=testRuntimeClasspath +com.github.jnr:jnr-posix:3.1.21=testRuntimeClasspath +com.github.jnr:jnr-unixsocket:0.38.24=testRuntimeClasspath +com.github.jnr:jnr-x86asm:1.0.2=testRuntimeClasspath +com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath +com.github.spotbugs:spotbugs:4.9.8=spotbugs +com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs +com.google.auto.service:auto-service-annotations:1.1.1=annotationProcessor,compileClasspath,testAnnotationProcessor,testCompileClasspath +com.google.auto.service:auto-service:1.1.1=annotationProcessor,testAnnotationProcessor +com.google.auto:auto-common:1.2.1=annotationProcessor,testAnnotationProcessor +com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,compileClasspath,spotbugs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.code.gson:gson:2.13.2=spotbugs +com.google.errorprone:error_prone_annotations:2.18.0=annotationProcessor,testAnnotationProcessor +com.google.errorprone:error_prone_annotations:2.41.0=spotbugs +com.google.guava:failureaccess:1.0.1=annotationProcessor,testAnnotationProcessor +com.google.guava:guava:20.0=testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.0.1-jre=annotationProcessor,testAnnotationProcessor +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,testAnnotationProcessor +com.google.j2objc:j2objc-annotations:2.8=annotationProcessor,testAnnotationProcessor +com.google.re2j:re2j:1.7=testRuntimeClasspath +com.squareup.moshi:moshi:1.11.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:logging-interceptor:3.12.12=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:3.12.12=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:1.17.5=testCompileClasspath,testRuntimeClasspath +com.thoughtworks.qdox:qdox:1.12.1=codenarc +commons-fileupload:commons-fileupload:1.5=testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.11.0=testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.20.0=spotbugs +de.thetaphi:forbiddenapis:3.10=compileClasspath +io.leangen.geantyref:geantyref:1.3.16=testRuntimeClasspath +io.sqreen:libsqreen:17.3.0=testRuntimeClasspath +javax.servlet:javax.servlet-api:3.1.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +jaxen:jaxen:2.0.0=spotbugs +junit:junit:4.13.2=testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.18.3=buildTimeInstrumentationPlugin,compileClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.3=buildTimeInstrumentationPlugin,compileClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna-platform:5.8.0=testRuntimeClasspath +net.java.dev.jna:jna:5.8.0=testRuntimeClasspath +net.sf.saxon:Saxon-HE:12.9=spotbugs +org.apache.ant:ant-antlr:1.10.14=codenarc +org.apache.ant:ant-junit:1.10.14=codenarc +org.apache.bcel:bcel:6.11.0=spotbugs +org.apache.commons:commons-lang3:3.19.0=spotbugs +org.apache.commons:commons-text:1.14.0=spotbugs +org.apache.logging.log4j:log4j-api:2.25.2=spotbugs +org.apache.logging.log4j:log4j-core:2.25.2=spotbugs +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.checkerframework:checker-qual:3.33.0=annotationProcessor,testAnnotationProcessor +org.codehaus.groovy:groovy-ant:3.0.23=codenarc +org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc +org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc +org.codehaus.groovy:groovy-json:3.0.23=codenarc +org.codehaus.groovy:groovy-json:3.0.25=testCompileClasspath,testRuntimeClasspath +org.codehaus.groovy:groovy-templates:3.0.23=codenarc +org.codehaus.groovy:groovy-xml:3.0.23=codenarc +org.codehaus.groovy:groovy:3.0.23=codenarc +org.codehaus.groovy:groovy:3.0.25=testCompileClasspath,testRuntimeClasspath +org.codenarc:CodeNarc:3.7.0=codenarc +org.dom4j:dom4j:2.2.0=spotbugs +org.eclipse.jetty:jetty-http:9.4.21.v20190926=compileClasspath +org.eclipse.jetty:jetty-io:9.4.21.v20190926=compileClasspath +org.eclipse.jetty:jetty-server:9.4.21.v20190926=compileClasspath +org.eclipse.jetty:jetty-util:9.4.21.v20190926=compileClasspath +org.gmetrics:GMetrics:2.1.0=codenarc +org.hamcrest:hamcrest-core:1.3=testRuntimeClasspath +org.hamcrest:hamcrest:3.0=testCompileClasspath,testRuntimeClasspath +org.jctools:jctools-core-jdk11:4.0.6=testRuntimeClasspath +org.jctools:jctools-core:4.0.6=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.14.1=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.14.1=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.14.1=testRuntimeClasspath +org.junit.platform:junit-platform-runner:1.14.1=testRuntimeClasspath +org.junit.platform:junit-platform-suite-api:1.14.1=testRuntimeClasspath +org.junit.platform:junit-platform-suite-commons:1.14.1=testRuntimeClasspath +org.junit:junit-bom:5.14.0=spotbugs +org.junit:junit-bom:5.14.1=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-core:4.4.0=testRuntimeClasspath +org.objenesis:objenesis:3.3=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.ow2.asm:asm-analysis:9.7.1=testRuntimeClasspath +org.ow2.asm:asm-analysis:9.9=spotbugs +org.ow2.asm:asm-commons:9.9=spotbugs +org.ow2.asm:asm-commons:9.9.1=testRuntimeClasspath +org.ow2.asm:asm-tree:9.9=spotbugs +org.ow2.asm:asm-tree:9.9.1=testRuntimeClasspath +org.ow2.asm:asm-util:9.7.1=testRuntimeClasspath +org.ow2.asm:asm-util:9.9=spotbugs +org.ow2.asm:asm:9.9=spotbugs +org.ow2.asm:asm:9.9.1=buildTimeInstrumentationPlugin,compileClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:jcl-over-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath +org.slf4j:jul-to-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath +org.slf4j:log4j-over-slf4j:1.7.30=testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:1.7.30=buildTimeInstrumentationPlugin,compileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath +org.slf4j:slf4j-api:1.7.32=testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.17=spotbugs,spotbugsSlf4j +org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j +org.snakeyaml:snakeyaml-engine:2.9=buildTimeInstrumentationPlugin,muzzleTooling,runtimeClasspath,testRuntimeClasspath +org.spockframework:spock-bom:2.4-groovy-3.0=testCompileClasspath,testRuntimeClasspath +org.spockframework:spock-core:2.4-groovy-3.0=testCompileClasspath,testRuntimeClasspath +org.tabletest:tabletest-junit:1.2.1=testCompileClasspath,testRuntimeClasspath +org.tabletest:tabletest-parser:1.2.0=testCompileClasspath,testRuntimeClasspath +org.xmlresolver:xmlresolver:5.3.3=spotbugs +empty=spotbugsPlugins diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/MultipartHelper.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/MultipartHelper.java new file mode 100644 index 00000000000..49a71f632dc --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/MultipartHelper.java @@ -0,0 +1,32 @@ +package datadog.trace.instrumentation.jetty94; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.servlet.http.Part; + +public class MultipartHelper { + + private MultipartHelper() {} + + /** + * Extracts non-null, non-empty filenames from a collection of multipart {@link Part}s using + * {@link Part#getSubmittedFileName()} (Servlet 3.1+, Jetty 9.4.x–10.x). + * + * @return list of filenames; never {@code null}, may be empty + */ + public static List extractFilenames(Collection parts) { + if (parts == null || parts.isEmpty()) { + return Collections.emptyList(); + } + List filenames = new ArrayList<>(); + for (Part part : parts) { + String name = part.getSubmittedFileName(); + if (name != null && !name.isEmpty()) { + filenames.add(name); + } + } + return filenames; + } +} diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/RequestExtractContentParametersInstrumentation.java new file mode 100644 index 00000000000..bc8283d3079 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/RequestExtractContentParametersInstrumentation.java @@ -0,0 +1,231 @@ +package datadog.trace.instrumentation.jetty94; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.api.gateway.Events.EVENTS; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +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.agent.tooling.muzzle.Reference; +import datadog.trace.api.gateway.BlockResponseFunction; +import datadog.trace.api.gateway.CallbackProvider; +import datadog.trace.api.gateway.Flow; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import java.util.Collection; +import java.util.List; +import java.util.function.BiFunction; +import javax.servlet.http.Part; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.MultiMap; + +@AutoService(InstrumenterModule.class) +public class RequestExtractContentParametersInstrumentation extends InstrumenterModule.AppSec + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + private static final String MULTI_MAP_INTERNAL_NAME = "Lorg/eclipse/jetty/util/MultiMap;"; + + public RequestExtractContentParametersInstrumentation() { + super("jetty"); + } + + @Override + public String instrumentedType() { + return "org.eclipse.jetty.server.Request"; + } + + @Override + public String[] helperClassNames() { + return new String[] {packageName + ".MultipartHelper"}; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("extractContentParameters").and(takesArguments(0)).or(named("getParts")), + getClass().getName() + "$ExtractContentParametersAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(1)), + getClass().getName() + "$GetFilenamesFromMultiPartAdvice"); + } + + // Discriminates Jetty 9.4–10.x ([9.4, 11.0)): + // - _contentParameters + extractContentParameters(void) exist from 9.3+ (excludes 9.2) + // - _multiParts exists from 9.4+ (excludes 9.3.x where only _multiPartInputStream existed) + // - javax.servlet.http.Part exists in 9.4–10.x classpath (excludes Jetty 11+ which uses jakarta) + private static final Reference REQUEST_REFERENCE = + new Reference.Builder("org.eclipse.jetty.server.Request") + .withMethod(new String[0], 0, "extractContentParameters", "V") + .withField(new String[0], 0, "_contentParameters", MULTI_MAP_INTERNAL_NAME) + .withField(new String[0], 0, "_multiParts", "Lorg/eclipse/jetty/server/MultiParts;") + .build(); + + private static final Reference JAVAX_PART_REFERENCE = + new Reference.Builder("javax.servlet.http.Part").build(); + + @Override + public Reference[] additionalMuzzleReferences() { + return new Reference[] {REQUEST_REFERENCE, JAVAX_PART_REFERENCE}; + } + + @RequiresRequestContext(RequestContextSlot.APPSEC) + public static class ExtractContentParametersAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static boolean before(@Advice.FieldValue("_contentParameters") final MultiMap map) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Request.class); + return callDepth == 0 && map == null; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Enter boolean proceed, + @Advice.FieldValue("_contentParameters") final MultiMap map, + @ActiveRequestContext RequestContext reqCtx, + @Advice.Thrown(readOnly = false) Throwable t) { + CallDepthThreadLocalMap.decrementCallDepth(Request.class); + if (!proceed) { + return; + } + if (map == null || map.isEmpty()) { + return; + } + + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction> callback = + cbp.getCallback(EVENTS.requestBodyProcessed()); + if (callback == null) { + return; + } + + Flow flow = callback.apply(reqCtx, map); + Flow.Action action = flow.getAction(); + if (action instanceof Flow.Action.RequestBlockingAction) { + Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action; + BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction(); + if (blockResponseFunction != null) { + blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba); + if (t == null) { + t = new BlockingException("Blocked request (for Request/extractContentParameters)"); + reqCtx.getTraceSegment().effectivelyBlocked(); + } + } + } + } + } + + /** + * Fires the {@code requestFilesFilenames} event when the application calls public {@code + * 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}: set by the first {@code getParts()} call in Jetty 9.4+; + * means filenames were already reported. + *
+ */ + @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", typing = Assigner.Typing.DYNAMIC) + final Object multiParts) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Collection.class); + return callDepth == 0 && contentParameters == null && multiParts == 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) { + CallDepthThreadLocalMap.decrementCallDepth(Collection.class); + if (!proceed || t != null || parts == null || parts.isEmpty()) { + return; + } + List filenames = MultipartHelper.extractFilenames(parts); + 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(); + } + } + } + } + } + + /** + * 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()}. + */ + @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; + } + List filenames = MultipartHelper.extractFilenames(parts); + 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.4/src/test/groovy/datadog/trace/instrumentation/jetty94/MultipartHelperTest.groovy b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/test/groovy/datadog/trace/instrumentation/jetty94/MultipartHelperTest.groovy new file mode 100644 index 00000000000..af027e5dd65 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/test/groovy/datadog/trace/instrumentation/jetty94/MultipartHelperTest.groovy @@ -0,0 +1,71 @@ +package datadog.trace.instrumentation.jetty94 + +import javax.servlet.http.Part +import spock.lang.Specification + +class MultipartHelperTest extends Specification { + + def "returns empty list for null collection"() { + expect: + MultipartHelper.extractFilenames(null) == [] + } + + def "returns empty list for empty collection"() { + expect: + MultipartHelper.extractFilenames([]) == [] + } + + def "returns empty list when all parts have null filename"() { + given: + def parts = [part(null), part(null)] + + expect: + MultipartHelper.extractFilenames(parts) == [] + } + + def "returns empty list when all parts have empty filename"() { + given: + def parts = [part(''), part('')] + + expect: + MultipartHelper.extractFilenames(parts) == [] + } + + def "extracts filename from single part"() { + given: + def parts = [part('photo.jpg')] + + expect: + MultipartHelper.extractFilenames(parts) == ['photo.jpg'] + } + + def "extracts filenames from multiple parts"() { + given: + def parts = [part('a.jpg'), part('b.png'), part('c.pdf')] + + expect: + MultipartHelper.extractFilenames(parts) == ['a.jpg', 'b.png', 'c.pdf'] + } + + def "skips parts with null or empty filename and keeps valid ones"() { + given: + def parts = [part(null), part('valid.txt'), part(''), part('other.zip')] + + expect: + MultipartHelper.extractFilenames(parts) == ['valid.txt', 'other.zip'] + } + + def "preserves filenames with spaces and special characters"() { + given: + def parts = [part('my file.tar.gz'), part('résumé.pdf')] + + expect: + MultipartHelper.extractFilenames(parts) == ['my file.tar.gz', 'résumé.pdf'] + } + + private Part part(String submittedFileName) { + Part p = Stub(Part) + p.getSubmittedFileName() >> submittedFileName + return p + } +} diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/build.gradle index 334d032273c..9bfd2d65e17 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/build.gradle @@ -63,7 +63,7 @@ dependencies { testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-7.0') testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-8.1.3') testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.2') - testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.3') + testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.4') // Include all websocket instrumentation modules for testing. Only the version-compatible module will apply at runtime. testRuntimeOnly project(':dd-java-agent:instrumentation:websocket:javax-websocket-1.0') testRuntimeOnly project(':dd-java-agent:instrumentation:websocket:jakarta-websocket-2.0') @@ -72,13 +72,13 @@ dependencies { latestDepTestImplementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '10.+' latestDepTestImplementation group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '10.+' latestDepTestImplementation group: 'org.eclipse.jetty.websocket', name: 'websocket-javax-server', version: '10.+' - latestDepTestImplementation project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.3') + latestDepTestImplementation project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.4') latestDepTestImplementation testFixtures(project(':dd-java-agent:instrumentation:servlet:javax-servlet:javax-servlet-3.0')) latestDepForkedTestImplementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '10.+' latestDepForkedTestImplementation group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '10.+' latestDepForkedTestImplementation group: 'org.eclipse.jetty.websocket', name: 'websocket-javax-server', version: '10.+' - latestDepForkedTestImplementation project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.3') + latestDepForkedTestImplementation project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.4') latestDepForkedTestImplementation testFixtures(project(':dd-java-agent:instrumentation:servlet:javax-servlet:javax-servlet-3.0')) } diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/build.gradle index 119dd38ea12..ae74abe7d1b 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/build.gradle @@ -47,7 +47,7 @@ dependencies { testImplementation ("org.eclipse.jetty.websocket:websocket-jakarta-server:11.0.0") { exclude group: 'org.slf4j', module: 'slf4j-api' } - testImplementation(project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.3')) + testImplementation(project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-11.0')) testImplementation testFixtures(project(':dd-java-agent:instrumentation:servlet:jakarta-servlet-5.0')) testImplementation project(':dd-java-agent:appsec:appsec-test-fixtures') testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-server:jetty-server-9.0') diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-12.0/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-12.0/build.gradle index d2346ef072a..25b4cd11e3f 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-12.0/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-12.0/build.gradle @@ -67,6 +67,8 @@ dependencies { } testImplementation(project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.3')) + testRuntimeOnly(project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.4')) + testRuntimeOnly(project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-11.0')) testImplementation testFixtures(project(':dd-java-agent:instrumentation:servlet:jakarta-servlet-5.0')) testImplementation testFixtures(project(':dd-java-agent:instrumentation:servlet:javax-servlet:javax-servlet-3.0')) testRuntimeOnly project(':dd-java-agent:instrumentation:websocket:javax-websocket-1.0') diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/build.gradle index 5d08c44f4c9..736630c640e 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.3/build.gradle @@ -41,6 +41,7 @@ dependencies { testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-8.1.3') testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.2') testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.3') + testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.4') latestDepTestImplementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '9.4.20.v20190813' latestDepTestImplementation group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '9.4.20.v20190813' diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/build.gradle index d9ef585146e..144358e05f5 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-9.4.21/build.gradle @@ -42,7 +42,7 @@ dependencies { testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-7.0') testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-8.1.3') testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.2') - testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.3') + testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.4') latestDepTestImplementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '9.+' latestDepTestImplementation group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '9.+' diff --git a/settings.gradle.kts b/settings.gradle.kts index 0bac052092a..7141c633195 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -414,6 +414,8 @@ include( ":dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-8.1.3", ":dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.2", ":dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.3", + ":dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.4", + ":dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-11.0", ":dd-java-agent:instrumentation:jetty:jetty-client:jetty-client-10.0", ":dd-java-agent:instrumentation:jetty:jetty-client:jetty-client-12.0", ":dd-java-agent:instrumentation:jetty:jetty-client:jetty-client-9.1", From 182ce986e1f414be7d1fd7232ddc5e61d1d9b0b1 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Fri, 10 Apr 2026 11:24:50 +0200 Subject: [PATCH 16/18] Add Jetty 8.x integration tests for multipart body and filenames - Add Jetty8LatestDepForkedTest: runs against Jetty 8.x (latestDepForkedTest task) and enables testBodyMultipart/testBodyFilenames coverage. Gated by 'test.dd.filenames' system property so it is skipped when running against Jetty 7.6. - Add testCompileOnly dep on org.eclipse.jetty.orbit:javax.servlet so MultipartConfigElement compiles without pulling in the excluded javax.servlet:javax.servlet-api artifact. - Fix ParameterCollector.put to accept (Object, Object) and cast internally: Jetty 8.x MultiMap.add uses (Object, Object) descriptor while Jetty 9.x uses (String, Object), so the ASM bytecode visitor was silently skipping all form field captures on Jetty 8. - Update GetPartsMethodVisitor to match both (String,Object) and (Object,Object) MultiMap.add descriptors and emit the INVOKEINTERFACE with (Object, Object). --- .../jetty8/ParameterCollector.java | 15 ++- .../RequestGetPartsInstrumentation.java | 6 +- .../jetty-server-7.6/build.gradle | 11 +++ .../jetty-server-7.6/gradle.lockfile | 53 +++++----- .../groovy/Jetty8LatestDepForkedTest.groovy | 96 +++++++++++++++++++ 5 files changed, 146 insertions(+), 35 deletions(-) create mode 100644 dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-7.6/src/test/groovy/Jetty8LatestDepForkedTest.groovy diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/ParameterCollector.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/ParameterCollector.java index 73e7038238e..f021d96025c 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/ParameterCollector.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/ParameterCollector.java @@ -9,7 +9,9 @@ public interface ParameterCollector { boolean isEmpty(); - void put(String key, String value); + // Takes Object to accommodate both Jetty 8.x (MultiMap.add(Object,Object)) and + // Jetty 9.x (MultiMap.add(String,Object)) bytecode call sites. + void put(Object key, Object value); Map> getMap(); @@ -24,7 +26,7 @@ public boolean isEmpty() { } @Override - public void put(String key, String value) {} + public void put(Object key, Object value) {} @Override public Map> getMap() { @@ -39,16 +41,19 @@ public boolean isEmpty() { return map == null; } - public void put(String key, String value) { + public void put(Object key, Object value) { + if (!(key instanceof String) || !(value instanceof String)) { + return; + } if (map == null) { map = new HashMap<>(); } List strings = map.get(key); if (strings == null) { strings = new ArrayList<>(); - map.put(key, strings); + map.put((String) key, strings); } - strings.add(value); + strings.add((String) value); } @Override 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 515f988e5bd..444082a113c 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 @@ -313,10 +313,12 @@ public GetPartsMethodVisitor(int api, MethodVisitor superMv, int collectedParams @Override public void visitMethodInsn( int opcode, String owner, String name, String descriptor, boolean isInterface) { + // Match MultiMap.add() in both Jetty 8.x (Object,Object) and Jetty 9.x (String,Object). if (opcode == Opcodes.INVOKEVIRTUAL && owner.equals("org/eclipse/jetty/util/MultiMap") && name.equals("add") - && descriptor.equals("(Ljava/lang/String;Ljava/lang/Object;)V")) { + && (descriptor.equals("(Ljava/lang/String;Ljava/lang/Object;)V") + || descriptor.equals("(Ljava/lang/Object;Ljava/lang/Object;)V"))) { super.visitVarInsn(Opcodes.ALOAD, collectedParamsVar); // stack: ..., key, value, collParams super.visitInsn(Opcodes.DUP_X2); @@ -329,7 +331,7 @@ public void visitMethodInsn( Opcodes.INVOKEINTERFACE, Type.getInternalName(ParameterCollector.class), "put", - "(Ljava/lang/String;Ljava/lang/String;)V", + "(Ljava/lang/Object;Ljava/lang/Object;)V", true); // original stack } diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-7.6/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-7.6/build.gradle index 613f00a4f3e..0b34ae39684 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-7.6/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-7.6/build.gradle @@ -20,6 +20,11 @@ configurations.testRuntimeOnly { exclude group: 'javax.servlet', module: 'javax.servlet-api' } +tasks.named('latestDepForkedTest', Test) { + // Signal that we are running against Jetty 8.x so Jetty8*LatestDepForkedTest activates. + systemProperty 'test.dd.filenames', 'true' +} + dependencies { compileOnly group: 'org.eclipse.jetty', name: 'jetty-server', version: '7.6.0.v20120127' implementation project(':dd-java-agent:instrumentation:jetty:jetty-common') @@ -34,8 +39,14 @@ dependencies { testImplementation group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '7.6.0.v20120127' testImplementation group: 'org.eclipse.jetty', name: 'jetty-continuation', version: '7.6.0.v20120127' testImplementation testFixtures(project(':dd-java-agent:instrumentation:servlet:javax-servlet:javax-servlet-3.0')) + // Needed to compile Jetty8LatestDepForkedTest (provides MultipartConfigElement). + // Uses the Orbit repackaging so it is not caught by the javax.servlet:javax.servlet-api exclusion. + // Compile-only: the Orbit jar is provided at runtime by Jetty 8.x in the latestDepForkedTest. + testCompileOnly group: 'org.eclipse.jetty.orbit', name: 'javax.servlet', version: '3.0.0.v201112011016' testRuntimeOnly project(':dd-java-agent:instrumentation:servlet:javax-servlet:javax-servlet-2.2') testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-7.0') + // Activated only on Jetty 8.x (muzzle rejects it for 7.6) + testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-8.1.3') latestDepTestImplementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '8.+' latestDepTestImplementation group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '8.+' diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-7.6/gradle.lockfile b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-7.6/gradle.lockfile index b6cc9477c24..c38e91494ca 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-7.6/gradle.lockfile +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-7.6/gradle.lockfile @@ -1,26 +1,26 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. -cafe.cryptography:curve25519-elisabeth:0.1.0=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -cafe.cryptography:ed25519-elisabeth:0.1.0=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +cafe.cryptography:curve25519-elisabeth:0.1.0=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath +cafe.cryptography:ed25519-elisabeth:0.1.0=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath ch.qos.logback:logback-classic:1.2.13=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath ch.qos.logback:logback-core:1.2.13=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath com.blogspot.mydailyjava:weak-lock-free:0.17=buildTimeInstrumentationPlugin,compileClasspath,latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq.okhttp3:okhttp:3.12.15=buildTimeInstrumentationPlugin,compileClasspath,latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq.okio:okio:1.17.6=buildTimeInstrumentationPlugin,compileClasspath,latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq.okhttp3:okhttp:3.12.15=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq.okio:okio:1.17.6=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath com.datadoghq:dd-instrument-java:0.0.3=buildTimeInstrumentationPlugin,compileClasspath,latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.datadoghq:dd-javac-plugin-client:0.2.2=buildTimeInstrumentationPlugin,compileClasspath,latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq:java-dogstatsd-client:4.4.3=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -com.datadoghq:sketches-java:0.8.3=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +com.datadoghq:java-dogstatsd-client:4.4.3=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath +com.datadoghq:sketches-java:0.8.3=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath com.github.javaparser:javaparser-core:3.25.6=codenarc -com.github.jnr:jffi:1.3.14=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-a64asm:1.0.0=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-constants:0.10.4=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-enxio:0.32.19=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-ffi:2.2.18=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-posix:3.1.21=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-unixsocket:0.38.24=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-x86asm:1.0.2=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +com.github.jnr:jffi:1.3.14=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-a64asm:1.0.0=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-constants:0.10.4=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-enxio:0.32.19=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-ffi:2.2.18=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-posix:3.1.21=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-unixsocket:0.38.24=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-x86asm:1.0.2=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath com.github.spotbugs:spotbugs:4.9.8=spotbugs com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs @@ -36,11 +36,11 @@ com.google.guava:guava:20.0=latestDepForkedTestCompileClasspath,latestDepForkedT com.google.guava:guava:32.0.1-jre=annotationProcessor,latestDepForkedTestAnnotationProcessor,testAnnotationProcessor com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,latestDepForkedTestAnnotationProcessor,testAnnotationProcessor com.google.j2objc:j2objc-annotations:2.8=annotationProcessor,latestDepForkedTestAnnotationProcessor,testAnnotationProcessor -com.google.re2j:re2j:1.7=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -com.squareup.moshi:moshi:1.11.0=buildTimeInstrumentationPlugin,compileClasspath,latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.re2j:re2j:1.7=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath +com.squareup.moshi:moshi:1.11.0=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath com.squareup.okhttp3:logging-interceptor:3.12.12=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath com.squareup.okhttp3:okhttp:3.12.12=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.squareup.okio:okio:1.17.5=buildTimeInstrumentationPlugin,compileClasspath,latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:1.17.5=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath com.thoughtworks.qdox:qdox:1.12.1=codenarc commons-fileupload:commons-fileupload:1.5=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.11.0=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -53,8 +53,8 @@ jaxen:jaxen:2.0.0=spotbugs junit:junit:4.13.2=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.18.3=buildTimeInstrumentationPlugin,compileClasspath,latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.18.3=buildTimeInstrumentationPlugin,compileClasspath,latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna-platform:5.8.0=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.8.0=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +net.java.dev.jna:jna-platform:5.8.0=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.8.0=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath net.sf.saxon:Saxon-HE:12.9=spotbugs org.apache.ant:ant-antlr:1.10.14=codenarc org.apache.ant:ant-junit:1.10.14=codenarc @@ -76,7 +76,7 @@ org.codehaus.groovy:groovy:3.0.23=codenarc org.codehaus.groovy:groovy:3.0.25=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.codenarc:CodeNarc:3.7.0=codenarc org.dom4j:dom4j:2.2.0=spotbugs -org.eclipse.jetty.orbit:javax.servlet:3.0.0.v201112011016=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,latestDepTestImplementation +org.eclipse.jetty.orbit:javax.servlet:3.0.0.v201112011016=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,latestDepTestImplementation,testCompileClasspath org.eclipse.jetty:jetty-continuation:7.6.0.v20120127=compileClasspath,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty:jetty-continuation:8.2.0.v20160908=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,latestDepTestImplementation org.eclipse.jetty:jetty-http:7.6.0.v20120127=compileClasspath,testCompileClasspath,testRuntimeClasspath @@ -94,8 +94,8 @@ org.eclipse.jetty:jetty-util:8.2.0.v20160908=latestDepForkedTestCompileClasspath org.gmetrics:GMetrics:2.1.0=codenarc org.hamcrest:hamcrest-core:1.3=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath org.hamcrest:hamcrest:3.0=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jctools:jctools-core-jdk11:4.0.6=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -org.jctools:jctools-core:4.0.6=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +org.jctools:jctools-core-jdk11:4.0.6=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath +org.jctools:jctools-core:4.0.6=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.14.1=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.14.1=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.14.1=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -111,19 +111,16 @@ org.junit:junit-bom:5.14.1=latestDepForkedTestCompileClasspath,latestDepForkedTe org.mockito:mockito-core:4.4.0=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath org.objenesis:objenesis:3.3=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.opentest4j:opentest4j:1.3.0=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.ow2.asm:asm-analysis:9.7.1=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +org.ow2.asm:asm-analysis:9.7.1=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath org.ow2.asm:asm-analysis:9.9=spotbugs -org.ow2.asm:asm-commons:9.7.1=buildTimeInstrumentationPlugin,muzzleTooling,runtimeClasspath org.ow2.asm:asm-commons:9.9=spotbugs org.ow2.asm:asm-commons:9.9.1=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath -org.ow2.asm:asm-tree:9.7.1=buildTimeInstrumentationPlugin,muzzleTooling,runtimeClasspath org.ow2.asm:asm-tree:9.9=spotbugs org.ow2.asm:asm-tree:9.9.1=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath -org.ow2.asm:asm-util:9.7.1=buildTimeInstrumentationPlugin,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +org.ow2.asm:asm-util:9.7.1=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath org.ow2.asm:asm-util:9.9=spotbugs -org.ow2.asm:asm:9.7.1=buildTimeInstrumentationPlugin,muzzleTooling,runtimeClasspath org.ow2.asm:asm:9.9=spotbugs -org.ow2.asm:asm:9.9.1=latestDepForkedTestRuntimeClasspath,testRuntimeClasspath +org.ow2.asm:asm:9.9.1=buildTimeInstrumentationPlugin,compileClasspath,latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:jcl-over-slf4j:1.7.30=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:1.7.30=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:log4j-over-slf4j:1.7.30=latestDepForkedTestCompileClasspath,latestDepForkedTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-7.6/src/test/groovy/Jetty8LatestDepForkedTest.groovy b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-7.6/src/test/groovy/Jetty8LatestDepForkedTest.groovy new file mode 100644 index 00000000000..f52ca00a39b --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-7.6/src/test/groovy/Jetty8LatestDepForkedTest.groovy @@ -0,0 +1,96 @@ +import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.agent.test.naming.TestingGenericHttpNamingConventions +import org.eclipse.jetty.server.Request +import org.eclipse.jetty.server.handler.AbstractHandler +import spock.lang.Requires + +import javax.servlet.MultipartConfigElement +import javax.servlet.ServletException +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +/** + * Integration tests for multipart filename extraction on Jetty 8.x. + * + *

Jetty 8.x introduced Servlet 3.0 and {@code getParts()}, which is the only entry point for + * multipart processing in this version range (there is no {@code extractContentParameters()} + * instrumentation like in 9.3+). The handler must therefore call {@code getParts()} explicitly + * before {@code getParameterMap()} so that multipart form fields are visible to the servlet. + * + *

Only activated for the {@code latestDepForkedTest} Gradle task (Jetty 8.x). The + * {@code test.dd.filenames} system property gates execution, preventing these tests from + * running against Jetty 7.6 where {@code getParts()} does not exist. + */ +abstract class Jetty8LatestDepForkedTest extends Jetty76Test { + + @Override + AbstractHandler handler() { + new Jetty8TestHandler() + } + + @Override + boolean testBodyMultipart() { + true + } + + @Override + boolean testBodyFilenames() { + true + } + + @Override + boolean testBodyFilenamesCalledOnce() { + true + } + + @Override + boolean testBodyFilenamesCalledOnceCombined() { + true + } + + static class Jetty8TestHandler extends AbstractHandler { + private static final MultipartConfigElement MULTIPART_CONFIG = + new MultipartConfigElement(System.getProperty('java.io.tmpdir')) + + @Override + void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + if (baseRequest.dispatcherType.name() != 'ERROR') { + // Enable Servlet 3.0 multipart processing for all requests. + request.setAttribute('org.eclipse.jetty.multipartConfig', MULTIPART_CONFIG) + request.setAttribute('org.eclipse.multipartConfig', MULTIPART_CONFIG) + + // Jetty 8.x does not populate getParameterMap() from multipart form fields without a + // prior getParts() call (unlike 9.3+ where extractContentParameters() does this). + // Pre-call getParts() for BODY_MULTIPART so the servlet can read form fields via + // getParameterMap(). Skip for BODY_MULTIPART_REPEATED and BODY_MULTIPART_COMBINED, + // which call getParts() themselves and rely on the first call triggering filenames. + def endpoint = HttpServerTest.ServerEndpoint.forPath(request.requestURI) + if (endpoint == HttpServerTest.ServerEndpoint.BODY_MULTIPART) { + try { + request.getParts() + } catch (IOException | ServletException ignored) {} + } + + Jetty76Test.TestHandler.handleRequest(baseRequest, response) + baseRequest.handled = true + } else { + Jetty76Test.errorHandler.handle(target, baseRequest, response, response) + } + } + } +} + +@Requires({ + System.getProperty('test.dd.filenames') +}) +class Jetty8V0LatestDepForkedTest extends Jetty8LatestDepForkedTest +implements TestingGenericHttpNamingConventions.ServerV0 { +} + +@Requires({ + System.getProperty('test.dd.filenames') +}) +class Jetty8V1LatestDepForkedTest extends Jetty8LatestDepForkedTest +implements TestingGenericHttpNamingConventions.ServerV1 { +} From d92ff947993eb9fa7700fedea793efaed81c1ce9 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Fri, 10 Apr 2026 12:42:58 +0200 Subject: [PATCH 17/18] Fix muzzle range for jetty-appsec-9.3/9.4: split at 9.4.10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _multiPartInputStream was replaced by _multiParts in Jetty 9.4.10.v20180503. Early 9.4.x versions (9.4.0–9.4.9) still use _multiPartInputStream like 9.3.x, so extend jetty-appsec-9.3 to cover [9.3, 9.4.10) and narrow jetty-appsec-9.4 to [9.4.10, 11.0). The classLoaderMatcher in jetty-appsec-9.4 (checking for _multiParts) now correctly matches only versions >= 9.4.10. --- .../jetty-appsec-9.3/build.gradle | 2 +- ...tractContentParametersInstrumentation.java | 5 +- .../jetty-appsec-9.4/build.gradle | 2 +- ...tractContentParametersInstrumentation.java | 54 +++++++++++++++++-- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/build.gradle index d32eda9a3eb..f151c5573aa 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.3/build.gradle @@ -2,7 +2,7 @@ muzzle { pass { group = 'org.eclipse.jetty' module = 'jetty-server' - versions = '[9.3,9.4)' + versions = '[9.3,9.4.10)' assertInverse = true } } 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 5ecd1d67435..f9912787b33 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 @@ -58,9 +58,10 @@ public void methodAdvice(MethodTransformer transformer) { getClass().getName() + "$GetFilenamesFromMultiPartAdvice"); } - // Discriminates Jetty 9.3.x ([9.3, 9.4)): + // Discriminates Jetty 9.3.x–9.4.9.x ([9.3, 9.4.10)): // - _contentParameters + extractContentParameters(void) exist from 9.3+ (excludes 9.2) - // - _multiPartInputStream exists only in 9.3.x (excludes 9.4+ where it became _multiParts) + // - _multiPartInputStream exists in 9.3.x and early 9.4.x (< 9.4.10); replaced by _multiParts + // in 9.4.10 (covered by jetty-appsec-9.4) private static final Reference REQUEST_REFERENCE = new Reference.Builder("org.eclipse.jetty.server.Request") .withMethod(new String[0], 0, "extractContentParameters", "V") diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/build.gradle index d2f68aec403..743f996de0d 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/build.gradle @@ -2,7 +2,7 @@ muzzle { pass { group = 'org.eclipse.jetty' module = 'jetty-server' - versions = '[9.4,11.0)' + versions = '[9.4.10,11.0)' assertInverse = true } } diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/RequestExtractContentParametersInstrumentation.java index bc8283d3079..e5b157b1b9d 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/RequestExtractContentParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/RequestExtractContentParametersInstrumentation.java @@ -18,12 +18,19 @@ import datadog.trace.api.gateway.RequestContextSlot; import datadog.trace.bootstrap.CallDepthThreadLocalMap; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import java.io.IOException; +import java.io.InputStream; import java.util.Collection; import java.util.List; import java.util.function.BiFunction; import javax.servlet.http.Part; import net.bytebuddy.asm.Advice; import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.jar.asm.ClassReader; +import net.bytebuddy.jar.asm.ClassVisitor; +import net.bytebuddy.jar.asm.FieldVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.matcher.ElementMatcher; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.MultiMap; @@ -58,15 +65,18 @@ public void methodAdvice(MethodTransformer transformer) { getClass().getName() + "$GetFilenamesFromMultiPartAdvice"); } - // Discriminates Jetty 9.4–10.x ([9.4, 11.0)): + // Discriminates Jetty 9.4.10–10.x ([9.4.10, 11.0)): // - _contentParameters + extractContentParameters(void) exist from 9.3+ (excludes 9.2) - // - _multiParts exists from 9.4+ (excludes 9.3.x where only _multiPartInputStream existed) // - javax.servlet.http.Part exists in 9.4–10.x classpath (excludes Jetty 11+ which uses jakarta) + // - classLoaderMatcher checks _multiParts field exists (any type) to exclude Jetty 9.3.x and + // early 9.4.x (< 9.4.10) which use _multiPartInputStream instead (covered by + // jetty-appsec-9.3). + // The _multiParts field type changed between 9.4.10 (MultiParts) and 10.x + // (MultiPartFormInputStream), so a typed muzzle reference cannot cover the full range. private static final Reference REQUEST_REFERENCE = new Reference.Builder("org.eclipse.jetty.server.Request") .withMethod(new String[0], 0, "extractContentParameters", "V") .withField(new String[0], 0, "_contentParameters", MULTI_MAP_INTERNAL_NAME) - .withField(new String[0], 0, "_multiParts", "Lorg/eclipse/jetty/server/MultiParts;") .build(); private static final Reference JAVAX_PART_REFERENCE = @@ -77,6 +87,44 @@ public Reference[] additionalMuzzleReferences() { return new Reference[] {REQUEST_REFERENCE, JAVAX_PART_REFERENCE}; } + /** Accepts classloaders where {@code Request._multiParts} field exists (any type). */ + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return MultiPartsFieldMatcher.INSTANCE; + } + + public static class MultiPartsFieldMatcher + extends ElementMatcher.Junction.ForNonNullValues { + public static final ElementMatcher.Junction INSTANCE = + new MultiPartsFieldMatcher(); + + @Override + protected boolean doMatch(ClassLoader cl) { + try (InputStream is = cl.getResourceAsStream("org/eclipse/jetty/server/Request.class")) { + if (is == null) { + return false; + } + ClassReader cr = new ClassReader(is); + final boolean[] found = {false}; + cr.accept( + new ClassVisitor(Opcodes.ASM9) { + @Override + public FieldVisitor visitField( + int access, String name, String descriptor, String signature, Object value) { + if ("_multiParts".equals(name)) { + found[0] = true; + } + return null; + } + }, + ClassReader.SKIP_CODE); + return found[0]; + } catch (IOException e) { + return false; + } + } + } + @RequiresRequestContext(RequestContextSlot.APPSEC) public static class ExtractContentParametersAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) From eb6ca7005bcb0df1e949467adec5a2c2df5137f7 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Fri, 10 Apr 2026 14:45:22 +0200 Subject: [PATCH 18/18] =?UTF-8?q?Replace=20MultiPartsFieldMatcher=20with?= =?UTF-8?q?=20typed=20module=20splits=20for=20Jetty=209.4=E2=80=9311.x?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The _multiParts field type changes multiple times across Jetty versions, making a single typed muzzle reference insufficient. Replace the ASM-based classLoaderMatcher with clean module splits using typed muzzle references: - jetty-appsec-9.4 [9.4.10, 10.0): _multiParts: MultiParts _queryEncoding: String (excludes 10.x) - jetty-appsec-10.0 [10.0, 10.0.10): _multiParts: MultiPartFormInputStream - jetty-appsec-10.0.10 [10.0.10, 11.0): _multiParts: MultiParts _queryEncoding: Charset (excludes 9.4.x) - jetty-appsec-11.0 [11.0, 11.0.10): _multiParts: MultiPartFormInputStream - jetty-appsec-11.0.10 [11.0.10, 12.0): _multiParts: MultiParts All six modules pass muzzle with assertInverse = true. --- .../jetty-appsec-10.0.10/build.gradle | 23 ++ .../jetty1010/MultipartHelper.java | 32 +++ ...tractContentParametersInstrumentation.java | 235 +++++++++++++++++ .../jetty-appsec-10.0/build.gradle | 23 ++ .../jetty10/MultipartHelper.java | 32 +++ ...tractContentParametersInstrumentation.java | 236 ++++++++++++++++++ .../jetty-appsec-11.0.10/build.gradle | 24 ++ .../jetty1110/MultipartHelper.java | 32 +++ ...tractContentParametersInstrumentation.java | 233 +++++++++++++++++ .../jetty-appsec-11.0/build.gradle | 2 +- ...tractContentParametersInstrumentation.java | 13 +- .../jetty-appsec-9.4/build.gradle | 2 +- ...tractContentParametersInstrumentation.java | 62 +---- .../jetty-server-10.0/build.gradle | 9 +- .../jetty-server-11.0/build.gradle | 1 + .../jetty-server-12.0/build.gradle | 1 + settings.gradle.kts | 3 + 17 files changed, 901 insertions(+), 62 deletions(-) create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0.10/build.gradle create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0.10/src/main/java/datadog/trace/instrumentation/jetty1010/MultipartHelper.java create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0.10/src/main/java/datadog/trace/instrumentation/jetty1010/RequestExtractContentParametersInstrumentation.java create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0/build.gradle create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0/src/main/java/datadog/trace/instrumentation/jetty10/MultipartHelper.java create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0/src/main/java/datadog/trace/instrumentation/jetty10/RequestExtractContentParametersInstrumentation.java create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0.10/build.gradle create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0.10/src/main/java/datadog/trace/instrumentation/jetty1110/MultipartHelper.java create mode 100644 dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0.10/src/main/java/datadog/trace/instrumentation/jetty1110/RequestExtractContentParametersInstrumentation.java diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0.10/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0.10/build.gradle new file mode 100644 index 00000000000..544f426941b --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0.10/build.gradle @@ -0,0 +1,23 @@ +muzzle { + pass { + group = 'org.eclipse.jetty' + module = 'jetty-server' + versions = '[10.0.10,11.0)' + assertInverse = true + javaVersion = 11 + } +} + +apply from: "$rootDir/gradle/java.gradle" + +dependencies { + compileOnly(group: 'org.eclipse.jetty', name: 'jetty-server', version: '10.0.26') { + exclude group: 'org.slf4j', module: 'slf4j-api' + } +} + +tasks.withType(JavaCompile).configureEach { + configureCompiler(it, 11, JavaVersion.VERSION_1_8) +} + +// testing happens in the jetty-* modules diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0.10/src/main/java/datadog/trace/instrumentation/jetty1010/MultipartHelper.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0.10/src/main/java/datadog/trace/instrumentation/jetty1010/MultipartHelper.java new file mode 100644 index 00000000000..7ae6899369e --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0.10/src/main/java/datadog/trace/instrumentation/jetty1010/MultipartHelper.java @@ -0,0 +1,32 @@ +package datadog.trace.instrumentation.jetty1010; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.servlet.http.Part; + +public class MultipartHelper { + + private MultipartHelper() {} + + /** + * Extracts non-null, non-empty filenames from a collection of multipart {@link Part}s using + * {@link Part#getSubmittedFileName()} (Servlet 3.1+, Jetty 10.0.10+). + * + * @return list of filenames; never {@code null}, may be empty + */ + public static List extractFilenames(Collection parts) { + if (parts == null || parts.isEmpty()) { + return Collections.emptyList(); + } + List filenames = new ArrayList<>(); + for (Part part : parts) { + String name = part.getSubmittedFileName(); + if (name != null && !name.isEmpty()) { + filenames.add(name); + } + } + return filenames; + } +} diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0.10/src/main/java/datadog/trace/instrumentation/jetty1010/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0.10/src/main/java/datadog/trace/instrumentation/jetty1010/RequestExtractContentParametersInstrumentation.java new file mode 100644 index 00000000000..77fac590b00 --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0.10/src/main/java/datadog/trace/instrumentation/jetty1010/RequestExtractContentParametersInstrumentation.java @@ -0,0 +1,235 @@ +package datadog.trace.instrumentation.jetty1010; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.api.gateway.Events.EVENTS; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +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.agent.tooling.muzzle.Reference; +import datadog.trace.api.gateway.BlockResponseFunction; +import datadog.trace.api.gateway.CallbackProvider; +import datadog.trace.api.gateway.Flow; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import java.util.Collection; +import java.util.List; +import java.util.function.BiFunction; +import javax.servlet.http.Part; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.MultiMap; + +@AutoService(InstrumenterModule.class) +public class RequestExtractContentParametersInstrumentation extends InstrumenterModule.AppSec + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + private static final String MULTI_MAP_INTERNAL_NAME = "Lorg/eclipse/jetty/util/MultiMap;"; + + public RequestExtractContentParametersInstrumentation() { + super("jetty"); + } + + @Override + public String instrumentedType() { + return "org.eclipse.jetty.server.Request"; + } + + @Override + public String[] helperClassNames() { + return new String[] {packageName + ".MultipartHelper"}; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("extractContentParameters").and(takesArguments(0)).or(named("getParts")), + getClass().getName() + "$ExtractContentParametersAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(1)), + getClass().getName() + "$GetFilenamesFromMultiPartAdvice"); + } + + // Discriminates Jetty 10.0.10–10.0.x ([10.0.10, 11.0)): + // - _contentParameters + extractContentParameters(void) exist from 9.3+ (excludes 9.2) + // - _multiParts: MultiParts exists in 10.0.10+ (excludes 10.0.0–10.0.9 where it was + // MultiPartFormInputStream, covered by jetty-appsec-10.0) + // - _queryEncoding: Charset exists in all 10.x (excludes 9.4.x where it is String, which also + // has _multiParts: MultiParts from 9.4.10+) + // - javax.servlet.http.Part exists in 10.x classpath (excludes Jetty 11+ which uses jakarta) + private static final Reference REQUEST_REFERENCE = + new Reference.Builder("org.eclipse.jetty.server.Request") + .withMethod(new String[0], 0, "extractContentParameters", "V") + .withField(new String[0], 0, "_contentParameters", MULTI_MAP_INTERNAL_NAME) + .withField(new String[0], 0, "_multiParts", "Lorg/eclipse/jetty/server/MultiParts;") + .withField(new String[0], 0, "_queryEncoding", "Ljava/nio/charset/Charset;") + .build(); + + private static final Reference JAVAX_PART_REFERENCE = + new Reference.Builder("javax.servlet.http.Part").build(); + + @Override + public Reference[] additionalMuzzleReferences() { + return new Reference[] {REQUEST_REFERENCE, JAVAX_PART_REFERENCE}; + } + + @RequiresRequestContext(RequestContextSlot.APPSEC) + public static class ExtractContentParametersAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static boolean before(@Advice.FieldValue("_contentParameters") final MultiMap map) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Request.class); + return callDepth == 0 && map == null; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Enter boolean proceed, + @Advice.FieldValue("_contentParameters") final MultiMap map, + @ActiveRequestContext RequestContext reqCtx, + @Advice.Thrown(readOnly = false) Throwable t) { + CallDepthThreadLocalMap.decrementCallDepth(Request.class); + if (!proceed) { + return; + } + if (map == null || map.isEmpty()) { + return; + } + + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction> callback = + cbp.getCallback(EVENTS.requestBodyProcessed()); + if (callback == null) { + return; + } + + Flow flow = callback.apply(reqCtx, map); + Flow.Action action = flow.getAction(); + if (action instanceof Flow.Action.RequestBlockingAction) { + Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action; + BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction(); + if (blockResponseFunction != null) { + blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba); + if (t == null) { + t = new BlockingException("Blocked request (for Request/extractContentParameters)"); + reqCtx.getTraceSegment().effectivelyBlocked(); + } + } + } + } + } + + /** + * Fires the {@code requestFilesFilenames} event when the application calls public {@code + * 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}: set by the first {@code getParts()} call in Jetty 10.0.10+; + * means filenames were already reported. + *
+ */ + @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", typing = Assigner.Typing.DYNAMIC) + final Object multiParts) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Collection.class); + return callDepth == 0 && contentParameters == null && multiParts == 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) { + CallDepthThreadLocalMap.decrementCallDepth(Collection.class); + if (!proceed || t != null || parts == null || parts.isEmpty()) { + return; + } + List filenames = MultipartHelper.extractFilenames(parts); + 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(); + } + } + } + } + } + + /** + * 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()}. + */ + @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; + } + List filenames = MultipartHelper.extractFilenames(parts); + 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-10.0/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0/build.gradle new file mode 100644 index 00000000000..e9d4c3c343c --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0/build.gradle @@ -0,0 +1,23 @@ +muzzle { + pass { + group = 'org.eclipse.jetty' + module = 'jetty-server' + versions = '[10.0,10.0.10)' + assertInverse = true + javaVersion = 11 + } +} + +apply from: "$rootDir/gradle/java.gradle" + +dependencies { + compileOnly(group: 'org.eclipse.jetty', name: 'jetty-server', version: '10.0.0') { + exclude group: 'org.slf4j', module: 'slf4j-api' + } +} + +tasks.withType(JavaCompile).configureEach { + configureCompiler(it, 11, JavaVersion.VERSION_1_8) +} + +// testing happens in the jetty-* modules diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0/src/main/java/datadog/trace/instrumentation/jetty10/MultipartHelper.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0/src/main/java/datadog/trace/instrumentation/jetty10/MultipartHelper.java new file mode 100644 index 00000000000..2d49f687acf --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0/src/main/java/datadog/trace/instrumentation/jetty10/MultipartHelper.java @@ -0,0 +1,32 @@ +package datadog.trace.instrumentation.jetty10; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.servlet.http.Part; + +public class MultipartHelper { + + private MultipartHelper() {} + + /** + * Extracts non-null, non-empty filenames from a collection of multipart {@link Part}s using + * {@link Part#getSubmittedFileName()} (Servlet 3.1+, Jetty 10.x). + * + * @return list of filenames; never {@code null}, may be empty + */ + public static List extractFilenames(Collection parts) { + if (parts == null || parts.isEmpty()) { + return Collections.emptyList(); + } + List filenames = new ArrayList<>(); + for (Part part : parts) { + String name = part.getSubmittedFileName(); + if (name != null && !name.isEmpty()) { + filenames.add(name); + } + } + return filenames; + } +} diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0/src/main/java/datadog/trace/instrumentation/jetty10/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0/src/main/java/datadog/trace/instrumentation/jetty10/RequestExtractContentParametersInstrumentation.java new file mode 100644 index 00000000000..a8788debabc --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-10.0/src/main/java/datadog/trace/instrumentation/jetty10/RequestExtractContentParametersInstrumentation.java @@ -0,0 +1,236 @@ +package datadog.trace.instrumentation.jetty10; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.api.gateway.Events.EVENTS; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +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.agent.tooling.muzzle.Reference; +import datadog.trace.api.gateway.BlockResponseFunction; +import datadog.trace.api.gateway.CallbackProvider; +import datadog.trace.api.gateway.Flow; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import java.util.Collection; +import java.util.List; +import java.util.function.BiFunction; +import javax.servlet.http.Part; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.MultiMap; + +@AutoService(InstrumenterModule.class) +public class RequestExtractContentParametersInstrumentation extends InstrumenterModule.AppSec + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + private static final String MULTI_MAP_INTERNAL_NAME = "Lorg/eclipse/jetty/util/MultiMap;"; + + public RequestExtractContentParametersInstrumentation() { + super("jetty"); + } + + @Override + public String instrumentedType() { + return "org.eclipse.jetty.server.Request"; + } + + @Override + public String[] helperClassNames() { + return new String[] {packageName + ".MultipartHelper"}; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("extractContentParameters").and(takesArguments(0)).or(named("getParts")), + getClass().getName() + "$ExtractContentParametersAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(1)), + getClass().getName() + "$GetFilenamesFromMultiPartAdvice"); + } + + // Discriminates Jetty 10.0.0–10.0.9 ([10.0, 10.0.10)): + // - _contentParameters + extractContentParameters(void) exist from 9.3+ (excludes 9.2) + // - _multiParts: MultiPartFormInputStream exists in 10.0.0–10.0.9 (excludes 9.4.x where it is + // MultiParts, and excludes 10.0.10+ where it reverted to MultiParts) + // - javax.servlet.http.Part exists in 10.x classpath (excludes Jetty 11+ which uses jakarta) + private static final Reference REQUEST_REFERENCE = + new Reference.Builder("org.eclipse.jetty.server.Request") + .withMethod(new String[0], 0, "extractContentParameters", "V") + .withField(new String[0], 0, "_contentParameters", MULTI_MAP_INTERNAL_NAME) + .withField( + new String[0], + 0, + "_multiParts", + "Lorg/eclipse/jetty/server/MultiPartFormInputStream;") + .build(); + + private static final Reference JAVAX_PART_REFERENCE = + new Reference.Builder("javax.servlet.http.Part").build(); + + @Override + public Reference[] additionalMuzzleReferences() { + return new Reference[] {REQUEST_REFERENCE, JAVAX_PART_REFERENCE}; + } + + @RequiresRequestContext(RequestContextSlot.APPSEC) + public static class ExtractContentParametersAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static boolean before(@Advice.FieldValue("_contentParameters") final MultiMap map) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Request.class); + return callDepth == 0 && map == null; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Enter boolean proceed, + @Advice.FieldValue("_contentParameters") final MultiMap map, + @ActiveRequestContext RequestContext reqCtx, + @Advice.Thrown(readOnly = false) Throwable t) { + CallDepthThreadLocalMap.decrementCallDepth(Request.class); + if (!proceed) { + return; + } + if (map == null || map.isEmpty()) { + return; + } + + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction> callback = + cbp.getCallback(EVENTS.requestBodyProcessed()); + if (callback == null) { + return; + } + + Flow flow = callback.apply(reqCtx, map); + Flow.Action action = flow.getAction(); + if (action instanceof Flow.Action.RequestBlockingAction) { + Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action; + BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction(); + if (blockResponseFunction != null) { + blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba); + if (t == null) { + t = new BlockingException("Blocked request (for Request/extractContentParameters)"); + reqCtx.getTraceSegment().effectivelyBlocked(); + } + } + } + } + } + + /** + * Fires the {@code requestFilesFilenames} event when the application calls public {@code + * 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}: set by the first {@code getParts()} call in Jetty 10.x; + * means filenames were already reported. + *
+ */ + @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", typing = Assigner.Typing.DYNAMIC) + final Object multiParts) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Collection.class); + return callDepth == 0 && contentParameters == null && multiParts == 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) { + CallDepthThreadLocalMap.decrementCallDepth(Collection.class); + if (!proceed || t != null || parts == null || parts.isEmpty()) { + return; + } + List filenames = MultipartHelper.extractFilenames(parts); + 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(); + } + } + } + } + } + + /** + * 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()}. + */ + @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; + } + List filenames = MultipartHelper.extractFilenames(parts); + 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-11.0.10/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0.10/build.gradle new file mode 100644 index 00000000000..8e4f1ec487f --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0.10/build.gradle @@ -0,0 +1,24 @@ +muzzle { + pass { + group = 'org.eclipse.jetty' + module = 'jetty-server' + versions = '[11.0.10,12.0)' + assertInverse = true + javaVersion = 11 + } +} + +apply from: "$rootDir/gradle/java.gradle" + +dependencies { + compileOnly(group: 'org.eclipse.jetty', name: 'jetty-server', version: '11.0.26') { + exclude group: 'org.slf4j', module: 'slf4j-api' + } + testImplementation(group: 'org.eclipse.jetty.toolchain', name: 'jetty-jakarta-servlet-api', version: '5.0.1') +} + +tasks.withType(JavaCompile).configureEach { + configureCompiler(it, 11, JavaVersion.VERSION_1_8) +} + +// testing happens in the jetty-* modules diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0.10/src/main/java/datadog/trace/instrumentation/jetty1110/MultipartHelper.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0.10/src/main/java/datadog/trace/instrumentation/jetty1110/MultipartHelper.java new file mode 100644 index 00000000000..3c233a763dd --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0.10/src/main/java/datadog/trace/instrumentation/jetty1110/MultipartHelper.java @@ -0,0 +1,32 @@ +package datadog.trace.instrumentation.jetty1110; + +import jakarta.servlet.http.Part; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class MultipartHelper { + + private MultipartHelper() {} + + /** + * Extracts non-null, non-empty filenames from a collection of multipart {@link Part}s using + * {@link Part#getSubmittedFileName()} (Servlet 5.0+, Jetty 11.0.10+). + * + * @return list of filenames; never {@code null}, may be empty + */ + public static List extractFilenames(Collection parts) { + if (parts == null || parts.isEmpty()) { + return Collections.emptyList(); + } + List filenames = new ArrayList<>(); + for (Part part : parts) { + String name = part.getSubmittedFileName(); + if (name != null && !name.isEmpty()) { + filenames.add(name); + } + } + return filenames; + } +} diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0.10/src/main/java/datadog/trace/instrumentation/jetty1110/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0.10/src/main/java/datadog/trace/instrumentation/jetty1110/RequestExtractContentParametersInstrumentation.java new file mode 100644 index 00000000000..2abd2b7f26a --- /dev/null +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0.10/src/main/java/datadog/trace/instrumentation/jetty1110/RequestExtractContentParametersInstrumentation.java @@ -0,0 +1,233 @@ +package datadog.trace.instrumentation.jetty1110; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.api.gateway.Events.EVENTS; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +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.agent.tooling.muzzle.Reference; +import datadog.trace.api.gateway.BlockResponseFunction; +import datadog.trace.api.gateway.CallbackProvider; +import datadog.trace.api.gateway.Flow; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import jakarta.servlet.http.Part; +import java.util.Collection; +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; + +@AutoService(InstrumenterModule.class) +public class RequestExtractContentParametersInstrumentation extends InstrumenterModule.AppSec + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + private static final String MULTI_MAP_INTERNAL_NAME = "Lorg/eclipse/jetty/util/MultiMap;"; + + public RequestExtractContentParametersInstrumentation() { + super("jetty"); + } + + @Override + public String instrumentedType() { + return "org.eclipse.jetty.server.Request"; + } + + @Override + public String[] helperClassNames() { + return new String[] {packageName + ".MultipartHelper"}; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("extractContentParameters").and(takesArguments(0)).or(named("getParts")), + getClass().getName() + "$ExtractContentParametersAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice"); + transformer.applyAdvice( + named("getParts").and(takesArguments(1)), + getClass().getName() + "$GetFilenamesFromMultiPartAdvice"); + } + + // Discriminates Jetty 11.0.10–11.0.x ([11.0.10, 12.0)): + // - _contentParameters + extractContentParameters(void) exist in 11.x (excludes Jetty 12 where + // org.eclipse.jetty.server.Request was removed) + // - _multiParts: MultiParts exists in 11.0.10+ (excludes 11.0.0–11.0.9 where it was + // MultiPartFormInputStream, covered by jetty-appsec-11.0) + // - jakarta.servlet.http.Part exists in 11.x classpath (excludes 9.4–10.x which use javax) + private static final Reference REQUEST_REFERENCE = + new Reference.Builder("org.eclipse.jetty.server.Request") + .withMethod(new String[0], 0, "extractContentParameters", "V") + .withField(new String[0], 0, "_contentParameters", MULTI_MAP_INTERNAL_NAME) + .withField(new String[0], 0, "_multiParts", "Lorg/eclipse/jetty/server/MultiParts;") + .build(); + + private static final Reference JAKARTA_PART_REFERENCE = + new Reference.Builder("jakarta.servlet.http.Part").build(); + + @Override + public Reference[] additionalMuzzleReferences() { + return new Reference[] {REQUEST_REFERENCE, JAKARTA_PART_REFERENCE}; + } + + @RequiresRequestContext(RequestContextSlot.APPSEC) + public static class ExtractContentParametersAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static boolean before(@Advice.FieldValue("_contentParameters") final MultiMap map) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Request.class); + return callDepth == 0 && map == null; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Enter boolean proceed, + @Advice.FieldValue("_contentParameters") final MultiMap map, + @ActiveRequestContext RequestContext reqCtx, + @Advice.Thrown(readOnly = false) Throwable t) { + CallDepthThreadLocalMap.decrementCallDepth(Request.class); + if (!proceed) { + return; + } + if (map == null || map.isEmpty()) { + return; + } + + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction> callback = + cbp.getCallback(EVENTS.requestBodyProcessed()); + if (callback == null) { + return; + } + + Flow flow = callback.apply(reqCtx, map); + Flow.Action action = flow.getAction(); + if (action instanceof Flow.Action.RequestBlockingAction) { + Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action; + BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction(); + if (blockResponseFunction != null) { + blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba); + if (t == null) { + t = new BlockingException("Blocked request (for Request/extractContentParameters)"); + reqCtx.getTraceSegment().effectivelyBlocked(); + } + } + } + } + } + + /** + * Fires the {@code requestFilesFilenames} event when the application calls public {@code + * 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}: set by the first {@code getParts()} call in Jetty 11.0.10+; + * means filenames were already reported. + *
+ */ + @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", typing = Assigner.Typing.DYNAMIC) + final Object multiParts) { + final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Collection.class); + return callDepth == 0 && contentParameters == null && multiParts == 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) { + CallDepthThreadLocalMap.decrementCallDepth(Collection.class); + if (!proceed || t != null || parts == null || parts.isEmpty()) { + return; + } + List filenames = MultipartHelper.extractFilenames(parts); + 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(); + } + } + } + } + } + + /** + * 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()}. + */ + @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; + } + List filenames = MultipartHelper.extractFilenames(parts); + 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-11.0/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/build.gradle index 718d98bd326..0df8039febe 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/build.gradle @@ -2,7 +2,7 @@ muzzle { pass { group = 'org.eclipse.jetty' module = 'jetty-server' - versions = '[11.0,12.0)' + versions = '[11.0,11.0.10)' assertInverse = true javaVersion = 11 } diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/main/java/datadog/trace/instrumentation/jetty11/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/main/java/datadog/trace/instrumentation/jetty11/RequestExtractContentParametersInstrumentation.java index 0a68d9d48c1..b3401a5884c 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/main/java/datadog/trace/instrumentation/jetty11/RequestExtractContentParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-11.0/src/main/java/datadog/trace/instrumentation/jetty11/RequestExtractContentParametersInstrumentation.java @@ -58,15 +58,20 @@ public void methodAdvice(MethodTransformer transformer) { getClass().getName() + "$GetFilenamesFromMultiPartAdvice"); } - // Discriminates Jetty 11.x ([11.0, 12.0)): + // Discriminates Jetty 11.0.0–11.0.9 ([11.0, 11.0.10)): // - _contentParameters + extractContentParameters(void) exist in 11.x (excludes Jetty 12) - // - _multiParts exists in 11.x + // - _multiParts: MultiPartFormInputStream exists in 11.0.0–11.0.9 (excludes 11.0.10+ where it + // reverted to MultiParts, covered by jetty-appsec-11.0.10) // - jakarta.servlet.http.Part exists in 11.x classpath (excludes 9.4–10.x which use javax) private static final Reference REQUEST_REFERENCE = new Reference.Builder("org.eclipse.jetty.server.Request") .withMethod(new String[0], 0, "extractContentParameters", "V") .withField(new String[0], 0, "_contentParameters", MULTI_MAP_INTERNAL_NAME) - .withField(new String[0], 0, "_multiParts", "Lorg/eclipse/jetty/server/MultiParts;") + .withField( + new String[0], + 0, + "_multiParts", + "Lorg/eclipse/jetty/server/MultiPartFormInputStream;") .build(); private static final Reference JAKARTA_PART_REFERENCE = @@ -130,7 +135,7 @@ static void after( *
  • {@code _contentParameters != null}: set by {@code extractContentParameters()} (the {@code * getParameterMap()} path); means filenames were already reported via {@code * GetFilenamesFromMultiPartAdvice}. - *
  • {@code _multiParts != null}: set by the first {@code getParts()} call in Jetty 11.x; + *
  • {@code _multiParts != null}: set by the first {@code getParts()} call in Jetty 11.0.x; * means filenames were already reported. * */ diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/build.gradle index 743f996de0d..a38fc1316b4 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/build.gradle @@ -2,7 +2,7 @@ muzzle { pass { group = 'org.eclipse.jetty' module = 'jetty-server' - versions = '[9.4.10,11.0)' + versions = '[9.4.10,10.0)' assertInverse = true } } diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/RequestExtractContentParametersInstrumentation.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/RequestExtractContentParametersInstrumentation.java index e5b157b1b9d..8d67aaba2a4 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/RequestExtractContentParametersInstrumentation.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-9.4/src/main/java/datadog/trace/instrumentation/jetty94/RequestExtractContentParametersInstrumentation.java @@ -18,19 +18,12 @@ import datadog.trace.api.gateway.RequestContextSlot; import datadog.trace.bootstrap.CallDepthThreadLocalMap; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; -import java.io.IOException; -import java.io.InputStream; import java.util.Collection; import java.util.List; import java.util.function.BiFunction; import javax.servlet.http.Part; import net.bytebuddy.asm.Advice; import net.bytebuddy.implementation.bytecode.assign.Assigner; -import net.bytebuddy.jar.asm.ClassReader; -import net.bytebuddy.jar.asm.ClassVisitor; -import net.bytebuddy.jar.asm.FieldVisitor; -import net.bytebuddy.jar.asm.Opcodes; -import net.bytebuddy.matcher.ElementMatcher; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.MultiMap; @@ -65,18 +58,19 @@ public void methodAdvice(MethodTransformer transformer) { getClass().getName() + "$GetFilenamesFromMultiPartAdvice"); } - // Discriminates Jetty 9.4.10–10.x ([9.4.10, 11.0)): + // Discriminates Jetty 9.4.10–9.4.x ([9.4.10, 10.0)): // - _contentParameters + extractContentParameters(void) exist from 9.3+ (excludes 9.2) - // - javax.servlet.http.Part exists in 9.4–10.x classpath (excludes Jetty 11+ which uses jakarta) - // - classLoaderMatcher checks _multiParts field exists (any type) to exclude Jetty 9.3.x and - // early 9.4.x (< 9.4.10) which use _multiPartInputStream instead (covered by - // jetty-appsec-9.3). - // The _multiParts field type changed between 9.4.10 (MultiParts) and 10.x - // (MultiPartFormInputStream), so a typed muzzle reference cannot cover the full range. + // - _multiParts: MultiParts exists in 9.4.10+ (excludes early 9.4.x covered by + // jetty-appsec-9.3, and excludes 10.0.0–10.0.9 where it is MultiPartFormInputStream) + // - _queryEncoding: String exists only in 9.4.x; changed to Charset in all 10.x (excludes + // 10.0.10+ where _multiParts reverted to MultiParts) + // - javax.servlet.http.Part exists in 9.4.x classpath (excludes Jetty 11+ which uses jakarta) private static final Reference REQUEST_REFERENCE = new Reference.Builder("org.eclipse.jetty.server.Request") .withMethod(new String[0], 0, "extractContentParameters", "V") .withField(new String[0], 0, "_contentParameters", MULTI_MAP_INTERNAL_NAME) + .withField(new String[0], 0, "_multiParts", "Lorg/eclipse/jetty/server/MultiParts;") + .withField(new String[0], 0, "_queryEncoding", "Ljava/lang/String;") .build(); private static final Reference JAVAX_PART_REFERENCE = @@ -87,44 +81,6 @@ public Reference[] additionalMuzzleReferences() { return new Reference[] {REQUEST_REFERENCE, JAVAX_PART_REFERENCE}; } - /** Accepts classloaders where {@code Request._multiParts} field exists (any type). */ - @Override - public ElementMatcher.Junction classLoaderMatcher() { - return MultiPartsFieldMatcher.INSTANCE; - } - - public static class MultiPartsFieldMatcher - extends ElementMatcher.Junction.ForNonNullValues { - public static final ElementMatcher.Junction INSTANCE = - new MultiPartsFieldMatcher(); - - @Override - protected boolean doMatch(ClassLoader cl) { - try (InputStream is = cl.getResourceAsStream("org/eclipse/jetty/server/Request.class")) { - if (is == null) { - return false; - } - ClassReader cr = new ClassReader(is); - final boolean[] found = {false}; - cr.accept( - new ClassVisitor(Opcodes.ASM9) { - @Override - public FieldVisitor visitField( - int access, String name, String descriptor, String signature, Object value) { - if ("_multiParts".equals(name)) { - found[0] = true; - } - return null; - } - }, - ClassReader.SKIP_CODE); - return found[0]; - } catch (IOException e) { - return false; - } - } - } - @RequiresRequestContext(RequestContextSlot.APPSEC) public static class ExtractContentParametersAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) @@ -178,7 +134,7 @@ static void after( *
  • {@code _contentParameters != null}: set by {@code extractContentParameters()} (the {@code * getParameterMap()} path); means filenames were already reported via {@code * GetFilenamesFromMultiPartAdvice}. - *
  • {@code _multiParts != null}: set by the first {@code getParts()} call in Jetty 9.4+; + *
  • {@code _multiParts != null}: set by the first {@code getParts()} call in Jetty 9.4.10+; * means filenames were already reported. * */ diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/build.gradle index 9bfd2d65e17..5ae70873e2c 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-10.0/build.gradle @@ -63,7 +63,8 @@ dependencies { testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-7.0') testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-8.1.3') testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.2') - testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.4') + testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-10.0') + testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-10.0.10') // Include all websocket instrumentation modules for testing. Only the version-compatible module will apply at runtime. testRuntimeOnly project(':dd-java-agent:instrumentation:websocket:javax-websocket-1.0') testRuntimeOnly project(':dd-java-agent:instrumentation:websocket:jakarta-websocket-2.0') @@ -72,13 +73,15 @@ dependencies { latestDepTestImplementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '10.+' latestDepTestImplementation group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '10.+' latestDepTestImplementation group: 'org.eclipse.jetty.websocket', name: 'websocket-javax-server', version: '10.+' - latestDepTestImplementation project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.4') + latestDepTestImplementation project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-10.0') + latestDepTestImplementation project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-10.0.10') latestDepTestImplementation testFixtures(project(':dd-java-agent:instrumentation:servlet:javax-servlet:javax-servlet-3.0')) latestDepForkedTestImplementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '10.+' latestDepForkedTestImplementation group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '10.+' latestDepForkedTestImplementation group: 'org.eclipse.jetty.websocket', name: 'websocket-javax-server', version: '10.+' - latestDepForkedTestImplementation project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.4') + latestDepForkedTestImplementation project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-10.0') + latestDepForkedTestImplementation project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-10.0.10') latestDepForkedTestImplementation testFixtures(project(':dd-java-agent:instrumentation:servlet:javax-servlet:javax-servlet-3.0')) } diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/build.gradle index ae74abe7d1b..b4f8ee26098 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-11.0/build.gradle @@ -48,6 +48,7 @@ dependencies { exclude group: 'org.slf4j', module: 'slf4j-api' } testImplementation(project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-11.0')) + testImplementation(project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-11.0.10')) testImplementation testFixtures(project(':dd-java-agent:instrumentation:servlet:jakarta-servlet-5.0')) testImplementation project(':dd-java-agent:appsec:appsec-test-fixtures') testRuntimeOnly project(':dd-java-agent:instrumentation:jetty:jetty-server:jetty-server-9.0') diff --git a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-12.0/build.gradle b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-12.0/build.gradle index 25b4cd11e3f..e0ad7398689 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-12.0/build.gradle +++ b/dd-java-agent/instrumentation/jetty/jetty-server/jetty-server-12.0/build.gradle @@ -69,6 +69,7 @@ dependencies { testImplementation(project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.3')) testRuntimeOnly(project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.4')) testRuntimeOnly(project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-11.0')) + testRuntimeOnly(project(':dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-11.0.10')) testImplementation testFixtures(project(':dd-java-agent:instrumentation:servlet:jakarta-servlet-5.0')) testImplementation testFixtures(project(':dd-java-agent:instrumentation:servlet:javax-servlet:javax-servlet-3.0')) testRuntimeOnly project(':dd-java-agent:instrumentation:websocket:javax-websocket-1.0') diff --git a/settings.gradle.kts b/settings.gradle.kts index 7141c633195..3ba09e4193b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -415,7 +415,10 @@ include( ":dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.2", ":dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.3", ":dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-9.4", + ":dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-10.0", + ":dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-10.0.10", ":dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-11.0", + ":dd-java-agent:instrumentation:jetty:jetty-appsec:jetty-appsec-11.0.10", ":dd-java-agent:instrumentation:jetty:jetty-client:jetty-client-10.0", ":dd-java-agent:instrumentation:jetty:jetty-client:jetty-client-12.0", ":dd-java-agent:instrumentation:jetty:jetty-client:jetty-client-9.1",