diff --git a/dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/build.gradle b/dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/build.gradle
index f5542d48255..1d3cc39c2c4 100644
--- a/dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/build.gradle
+++ b/dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/build.gradle
@@ -15,6 +15,13 @@ muzzle {
extraDependency 'org.apache.tomcat:tomcat-catalina:7.0.4'
assertInverse = true
}
+ pass {
+ name = 'glassfish'
+ group = 'org.glassfish.main.extras'
+ module = 'glassfish-embedded-all'
+ versions = '[4.0, 6.1.0)' // GlassFish 6.1.0+ uses jakarta.* namespace; our advice uses javax.servlet.http.Part
+ assertInverse = true
+ }
}
apply from: "$rootDir/gradle/java.gradle"
@@ -22,10 +29,15 @@ apply from: "$rootDir/gradle/java.gradle"
dependencies {
compileOnly group: 'org.apache.tomcat', name: 'tomcat-catalina', version: '7.0.4'
compileOnly group: 'org.apache.tomcat', name: 'tomcat-coyote', version: '7.0.4'
+ // Servlet 3.1 API needed to reference Part.getSubmittedFileName() in GlassFishMultipartInstrumentation.
+ // tomcat-catalina:7.0.4 provides only Servlet 3.0 (no getSubmittedFileName); GlassFish 4+ is Servlet 3.1.
+ compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
implementation project(':dd-java-agent:instrumentation:tomcat:tomcat-common')
testImplementation group: 'org.apache.tomcat', name: 'tomcat-catalina', version: '7.0.4'
testImplementation group: 'org.apache.tomcat', name: 'tomcat-coyote', version: '7.0.4'
+ testImplementation group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
+ testImplementation libs.bundles.mockito
}
// testing happens in tomcat-5.5 module
diff --git a/dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/src/main/java/datadog/trace/instrumentation/tomcat7/GlassFishBlockingHelper.java b/dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/src/main/java/datadog/trace/instrumentation/tomcat7/GlassFishBlockingHelper.java
new file mode 100644
index 00000000000..5062468ae8d
--- /dev/null
+++ b/dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/src/main/java/datadog/trace/instrumentation/tomcat7/GlassFishBlockingHelper.java
@@ -0,0 +1,84 @@
+package datadog.trace.instrumentation.tomcat7;
+
+import datadog.appsec.api.blocking.BlockingContentType;
+import datadog.trace.api.Config;
+import datadog.trace.api.gateway.BlockResponseFunction;
+import datadog.trace.api.gateway.Flow;
+import datadog.trace.api.gateway.RequestContext;
+import datadog.trace.bootstrap.blocking.BlockingActionHelper;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public final class GlassFishBlockingHelper {
+
+ public static final int MAX_FILE_CONTENT_COUNT = Config.get().getAppSecMaxFileContentCount();
+ public static final int MAX_FILE_CONTENT_BYTES = Config.get().getAppSecMaxFileContentBytes();
+
+ /**
+ * Attempts to commit a blocking response via the registered {@link BlockResponseFunction} or via
+ * the Servlet API fallback, then marks the trace segment as effectively blocked.
+ *
+ *
Returns {@code true} if the response was committed (regardless of whether {@link
+ * datadog.trace.api.internal.TraceSegment#effectivelyBlocked()} succeeded). Returns {@code false}
+ * if no response could be committed.
+ */
+ public static boolean tryBlock(
+ RequestContext reqCtx,
+ HttpServletRequest fallbackReq,
+ HttpServletResponse fallbackResp,
+ Flow.Action.RequestBlockingAction rba) {
+ try {
+ BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
+ if (brf != null) {
+ brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
+ } else if (!commitBlocking(fallbackReq, fallbackResp, rba)) {
+ return false;
+ }
+ } catch (Exception ignored) {
+ return false;
+ }
+ // Response was committed — mark as blocked on a best-effort basis.
+ // effectivelyBlocked() can throw if the span is already finished; that must not suppress the
+ // true return value since the response has already been sent to the client.
+ try {
+ reqCtx.getTraceSegment().effectivelyBlocked();
+ } catch (Exception ignored) {
+ }
+ return true;
+ }
+
+ public static boolean commitBlocking(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ Flow.Action.RequestBlockingAction rba) {
+ if (response == null) {
+ return false;
+ }
+ try {
+ if (response.isCommitted()) {
+ return false;
+ }
+ response.reset();
+ response.setStatus(BlockingActionHelper.getHttpCode(rba.getStatusCode()));
+ for (Map.Entry e : rba.getExtraHeaders().entrySet()) {
+ response.setHeader(e.getKey(), e.getValue());
+ }
+ if (rba.getBlockingContentType() != BlockingContentType.NONE) {
+ String accept = request != null ? request.getHeader("Accept") : null;
+ BlockingActionHelper.TemplateType type =
+ BlockingActionHelper.determineTemplateType(rba.getBlockingContentType(), accept);
+ byte[] body = BlockingActionHelper.getTemplate(type, rba.getSecurityResponseId());
+ if (body != null) {
+ response.setHeader("Content-Type", BlockingActionHelper.getContentType(type));
+ response.setHeader("Content-Length", Integer.toString(body.length));
+ response.getOutputStream().write(body);
+ }
+ }
+ response.flushBuffer();
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git a/dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/src/main/java/datadog/trace/instrumentation/tomcat7/GlassFishMultipartInstrumentation.java b/dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/src/main/java/datadog/trace/instrumentation/tomcat7/GlassFishMultipartInstrumentation.java
new file mode 100644
index 00000000000..779b2aed8c4
--- /dev/null
+++ b/dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/src/main/java/datadog/trace/instrumentation/tomcat7/GlassFishMultipartInstrumentation.java
@@ -0,0 +1,198 @@
+package datadog.trace.instrumentation.tomcat7;
+
+import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
+import static datadog.trace.api.gateway.Events.EVENTS;
+import static net.bytebuddy.matcher.ElementMatchers.isPublic;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import com.google.auto.service.AutoService;
+import datadog.trace.agent.tooling.Instrumenter;
+import datadog.trace.agent.tooling.InstrumenterModule;
+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.api.http.MultipartContentDecoder;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiFunction;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.Part;
+import net.bytebuddy.asm.Advice;
+
+/**
+ * GlassFish/Payara does not have {@code Request.parseParts()} — instead {@code Request.getParts()}
+ * delegates to {@code org.apache.catalina.fileupload.Multipart.getParts()}. This instrumentation
+ * hooks that GlassFish-specific class to report uploaded file names and contents to the AppSec WAF
+ * via the {@code requestFilesFilenames} and {@code requestFilesContent} IG events.
+ *
+ * Because {@code org.apache.catalina.fileupload.Multipart} does not exist in standard Tomcat,
+ * this instrumentation is automatically skipped by ByteBuddy on non-GlassFish containers.
+ *
+ *
This advice casts each {@code Part} through the {@code javax.servlet.http.Part} interface
+ * (which {@code org.apache.catalina.fileupload.PartItem} implements) to avoid Java module-system
+ * access restrictions that prevent reflective invocation of methods on GlassFish-internal classes.
+ */
+@AutoService(InstrumenterModule.class)
+public class GlassFishMultipartInstrumentation extends InstrumenterModule.AppSec
+ implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
+
+ public GlassFishMultipartInstrumentation() {
+ super("tomcat");
+ }
+
+ @Override
+ public String muzzleDirective() {
+ return "glassfish";
+ }
+
+ @Override
+ public String instrumentedType() {
+ return "org.apache.catalina.fileupload.Multipart";
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {
+ "datadog.trace.instrumentation.tomcat7.GlassFishBlockingHelper",
+ };
+ }
+
+ @Override
+ public void methodAdvice(MethodTransformer transformer) {
+ transformer.applyAdvice(
+ named("getParts").and(takesArguments(0)).and(isPublic()),
+ getClass().getName() + "$GetPartsAdvice");
+ }
+
+ public static class GetPartsAdvice {
+
+ @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
+ static void after(
+ @Advice.This Object thisMultipart,
+ @Advice.Return(readOnly = false) Collection> parts,
+ @Advice.Thrown Throwable t) {
+ if (t != null || parts == null || parts.isEmpty()) {
+ return;
+ }
+
+ AgentSpan agentSpan = AgentTracer.activeSpan();
+ if (agentSpan == null) {
+ return;
+ }
+ RequestContext reqCtx = agentSpan.getRequestContext();
+ if (reqCtx == null || reqCtx.getData(RequestContextSlot.APPSEC) == null) {
+ return;
+ }
+
+ CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
+ BiFunction, Flow> filenamesCb =
+ cbp.getCallback(EVENTS.requestFilesFilenames());
+ BiFunction, Flow> contentCb =
+ cbp.getCallback(EVENTS.requestFilesContent());
+ if (filenamesCb == null && contentCb == null) {
+ return;
+ }
+
+ // Extract servlet request/response for fallback blocking when no BlockResponseFunction is
+ // registered (Payara: TomcatServerInstrumentation is muzzled out for Payara's response type).
+ // setAccessible works here because this code is inlined into Multipart.getParts() —
+ // the same module as the private field's owner class.
+ HttpServletRequest fallbackReq = null;
+ HttpServletResponse fallbackResp = null;
+ try {
+ Field f = thisMultipart.getClass().getDeclaredField("request");
+ f.setAccessible(true);
+ Object catReq = f.get(thisMultipart);
+ if (catReq instanceof HttpServletRequest) {
+ fallbackReq = (HttpServletRequest) catReq;
+ }
+ if (catReq != null) {
+ Method m = catReq.getClass().getMethod("getResponse");
+ Object catResp = m.invoke(catReq);
+ if (catResp instanceof HttpServletResponse) {
+ fallbackResp = (HttpServletResponse) catResp;
+ }
+ }
+ } catch (Exception ignored) {
+ }
+
+ int maxFiles = GlassFishBlockingHelper.MAX_FILE_CONTENT_COUNT;
+ int maxBytes = GlassFishBlockingHelper.MAX_FILE_CONTENT_BYTES;
+
+ List filenames = null;
+ List contents = null;
+
+ for (Object partObj : parts) {
+ try {
+ if (!(partObj instanceof Part)) {
+ continue;
+ }
+ Part part = (Part) partObj;
+ String filename = part.getSubmittedFileName();
+ // null means no filename parameter → form field, skip
+ // empty string means filename="" was sent → file upload without a name
+ if (filename == null) {
+ continue;
+ }
+ if (filenamesCb != null && !filename.isEmpty()) {
+ if (filenames == null) {
+ filenames = new ArrayList<>();
+ }
+ filenames.add(filename);
+ }
+ if (contentCb != null) {
+ if (contents == null) {
+ contents = new ArrayList<>();
+ }
+ if (contents.size() < maxFiles) {
+ try (InputStream is = part.getInputStream()) {
+ contents.add(
+ MultipartContentDecoder.readInputStream(is, maxBytes, part.getContentType()));
+ } catch (Exception ignored) {
+ contents.add("");
+ }
+ }
+ }
+ } catch (Exception ignored) {
+ }
+ }
+
+ boolean blocked = false;
+
+ if (filenames != null && !filenames.isEmpty() && filenamesCb != null) {
+ Flow flow = filenamesCb.apply(reqCtx, filenames);
+ Flow.Action action = flow.getAction();
+ if (action instanceof Flow.Action.RequestBlockingAction) {
+ if (GlassFishBlockingHelper.tryBlock(
+ reqCtx, fallbackReq, fallbackResp, (Flow.Action.RequestBlockingAction) action)) {
+ parts = Collections.emptyList();
+ blocked = true;
+ }
+ }
+ }
+
+ if (!blocked && contents != null && !contents.isEmpty() && contentCb != null) {
+ Flow contentFlow = contentCb.apply(reqCtx, contents);
+ Flow.Action contentAction = contentFlow.getAction();
+ if (contentAction instanceof Flow.Action.RequestBlockingAction) {
+ if (GlassFishBlockingHelper.tryBlock(
+ reqCtx,
+ fallbackReq,
+ fallbackResp,
+ (Flow.Action.RequestBlockingAction) contentAction)) {
+ parts = Collections.emptyList();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/src/test/java/datadog/trace/instrumentation/tomcat7/GlassFishBlockingHelperTest.java b/dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/src/test/java/datadog/trace/instrumentation/tomcat7/GlassFishBlockingHelperTest.java
new file mode 100644
index 00000000000..c7d046d8756
--- /dev/null
+++ b/dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/src/test/java/datadog/trace/instrumentation/tomcat7/GlassFishBlockingHelperTest.java
@@ -0,0 +1,211 @@
+package datadog.trace.instrumentation.tomcat7;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import datadog.appsec.api.blocking.BlockingContentType;
+import datadog.trace.api.gateway.BlockResponseFunction;
+import datadog.trace.api.gateway.Flow;
+import datadog.trace.api.gateway.RequestContext;
+import datadog.trace.api.internal.TraceSegment;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.jupiter.api.Test;
+
+class GlassFishBlockingHelperTest {
+
+ // ------- commitBlocking() -------
+
+ @Test
+ void commitBlocking_nullResponse_returnsFalse() {
+ assertFalse(GlassFishBlockingHelper.commitBlocking(null, null, rba(403)));
+ }
+
+ @Test
+ void commitBlocking_committedResponse_returnsFalse() {
+ HttpServletResponse resp = mock(HttpServletResponse.class);
+ when(resp.isCommitted()).thenReturn(true);
+ assertFalse(GlassFishBlockingHelper.commitBlocking(null, resp, rba(403)));
+ }
+
+ @Test
+ void commitBlocking_blockingContentTypeNone_setsStatusWithoutBody() throws IOException {
+ HttpServletResponse resp = mock(HttpServletResponse.class);
+ when(resp.isCommitted()).thenReturn(false);
+
+ assertTrue(
+ GlassFishBlockingHelper.commitBlocking(
+ null, resp, new Flow.Action.RequestBlockingAction(403, BlockingContentType.NONE)));
+
+ verify(resp).setStatus(403);
+ verify(resp).flushBuffer();
+ verify(resp, never()).setHeader(eq("Content-Type"), any());
+ verify(resp, never()).getOutputStream();
+ }
+
+ @Test
+ void commitBlocking_withJsonAccept_writesJsonBody() throws IOException {
+ HttpServletRequest req = mock(HttpServletRequest.class);
+ when(req.getHeader("Accept")).thenReturn("application/json");
+ TestServletOutputStream out = new TestServletOutputStream();
+ HttpServletResponse resp = mock(HttpServletResponse.class);
+ when(resp.isCommitted()).thenReturn(false);
+ when(resp.getOutputStream()).thenReturn(out);
+
+ assertTrue(GlassFishBlockingHelper.commitBlocking(req, resp, rba(403)));
+
+ verify(resp).setStatus(403);
+ verify(resp).setHeader(eq("Content-Type"), contains("json"));
+ verify(resp).setHeader(eq("Content-Length"), any());
+ assertTrue(out.getBytes().length > 0);
+ verify(resp).flushBuffer();
+ }
+
+ @Test
+ void commitBlocking_withHtmlAccept_writesHtmlBody() throws IOException {
+ HttpServletRequest req = mock(HttpServletRequest.class);
+ when(req.getHeader("Accept")).thenReturn("text/html");
+ TestServletOutputStream out = new TestServletOutputStream();
+ HttpServletResponse resp = mock(HttpServletResponse.class);
+ when(resp.isCommitted()).thenReturn(false);
+ when(resp.getOutputStream()).thenReturn(out);
+
+ assertTrue(GlassFishBlockingHelper.commitBlocking(req, resp, rba(403)));
+
+ verify(resp).setHeader(eq("Content-Type"), contains("html"));
+ assertTrue(out.getBytes().length > 0);
+ }
+
+ @Test
+ void commitBlocking_nullRequest_defaultsToJsonBody() throws IOException {
+ TestServletOutputStream out = new TestServletOutputStream();
+ HttpServletResponse resp = mock(HttpServletResponse.class);
+ when(resp.isCommitted()).thenReturn(false);
+ when(resp.getOutputStream()).thenReturn(out);
+
+ assertTrue(GlassFishBlockingHelper.commitBlocking(null, resp, rba(403)));
+
+ verify(resp).setStatus(403);
+ assertTrue(out.getBytes().length > 0);
+ }
+
+ @Test
+ void commitBlocking_ioException_returnsFalse() throws IOException {
+ HttpServletResponse resp = mock(HttpServletResponse.class);
+ when(resp.isCommitted()).thenReturn(false);
+ when(resp.getOutputStream()).thenThrow(new IOException("stream error"));
+
+ assertFalse(GlassFishBlockingHelper.commitBlocking(null, resp, rba(403)));
+ }
+
+ // ------- tryBlock() -------
+
+ @Test
+ void tryBlock_withBrf_commitsViaFunctionAndReturnsTrue() throws Exception {
+ TraceSegment segment = mock(TraceSegment.class);
+ BlockResponseFunction brf = mock(BlockResponseFunction.class);
+ RequestContext reqCtx = mockReqCtx(brf, segment);
+
+ Flow.Action.RequestBlockingAction action = rba(403);
+ assertTrue(GlassFishBlockingHelper.tryBlock(reqCtx, null, null, action));
+
+ verify(brf).tryCommitBlockingResponse(segment, action);
+ verify(segment).effectivelyBlocked();
+ }
+
+ @Test
+ void tryBlock_noBrf_fallbackSucceeds_returnsTrue() throws IOException {
+ TraceSegment segment = mock(TraceSegment.class);
+ RequestContext reqCtx = mockReqCtx(null, segment);
+ TestServletOutputStream out = new TestServletOutputStream();
+ HttpServletResponse resp = mock(HttpServletResponse.class);
+ when(resp.isCommitted()).thenReturn(false);
+ when(resp.getOutputStream()).thenReturn(out);
+
+ assertTrue(GlassFishBlockingHelper.tryBlock(reqCtx, null, resp, rba(403)));
+ verify(segment).effectivelyBlocked();
+ }
+
+ @Test
+ void tryBlock_noBrf_nullFallbackResponse_returnsFalse() {
+ RequestContext reqCtx = mock(RequestContext.class);
+ when(reqCtx.getBlockResponseFunction()).thenReturn(null);
+
+ assertFalse(GlassFishBlockingHelper.tryBlock(reqCtx, null, null, rba(403)));
+ verify(reqCtx, never()).getTraceSegment();
+ }
+
+ @Test
+ void tryBlock_brfThrows_returnsFalse() throws Exception {
+ TraceSegment segment = mock(TraceSegment.class);
+ BlockResponseFunction brf = mock(BlockResponseFunction.class);
+ RequestContext reqCtx = mockReqCtx(brf, segment);
+ doThrow(new RuntimeException("commit failed"))
+ .when(brf)
+ .tryCommitBlockingResponse(any(), any(Flow.Action.RequestBlockingAction.class));
+
+ assertFalse(GlassFishBlockingHelper.tryBlock(reqCtx, null, null, rba(403)));
+ verify(segment, never()).effectivelyBlocked();
+ }
+
+ @Test
+ void tryBlock_effectivelyBlockedThrows_stillReturnsTrue() throws Exception {
+ TraceSegment segment = mock(TraceSegment.class);
+ BlockResponseFunction brf = mock(BlockResponseFunction.class);
+ RequestContext reqCtx = mockReqCtx(brf, segment);
+ doThrow(new RuntimeException("span already finished")).when(segment).effectivelyBlocked();
+
+ assertTrue(GlassFishBlockingHelper.tryBlock(reqCtx, null, null, rba(403)));
+ }
+
+ // ------- Helpers -------
+
+ private static Flow.Action.RequestBlockingAction rba(int statusCode) {
+ return new Flow.Action.RequestBlockingAction(statusCode, BlockingContentType.AUTO);
+ }
+
+ private static RequestContext mockReqCtx(BlockResponseFunction brf, TraceSegment segment) {
+ RequestContext reqCtx = mock(RequestContext.class);
+ when(reqCtx.getBlockResponseFunction()).thenReturn(brf);
+ when(reqCtx.getTraceSegment()).thenReturn(segment);
+ return reqCtx;
+ }
+
+ private static final class TestServletOutputStream extends ServletOutputStream {
+ private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void setWriteListener(WriteListener listener) {}
+
+ @Override
+ public void write(int b) throws IOException {
+ buffer.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ buffer.write(b, off, len);
+ }
+
+ public byte[] getBytes() {
+ return buffer.toByteArray();
+ }
+ }
+}