Skip to content

Commit eb6ca70

Browse files
committed
Replace MultiPartsFieldMatcher with typed module splits for Jetty 9.4–11.x
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.
1 parent d92ff94 commit eb6ca70

File tree

17 files changed

+901
-62
lines changed

17 files changed

+901
-62
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
muzzle {
2+
pass {
3+
group = 'org.eclipse.jetty'
4+
module = 'jetty-server'
5+
versions = '[10.0.10,11.0)'
6+
assertInverse = true
7+
javaVersion = 11
8+
}
9+
}
10+
11+
apply from: "$rootDir/gradle/java.gradle"
12+
13+
dependencies {
14+
compileOnly(group: 'org.eclipse.jetty', name: 'jetty-server', version: '10.0.26') {
15+
exclude group: 'org.slf4j', module: 'slf4j-api'
16+
}
17+
}
18+
19+
tasks.withType(JavaCompile).configureEach {
20+
configureCompiler(it, 11, JavaVersion.VERSION_1_8)
21+
}
22+
23+
// testing happens in the jetty-* modules
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package datadog.trace.instrumentation.jetty1010;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collection;
5+
import java.util.Collections;
6+
import java.util.List;
7+
import javax.servlet.http.Part;
8+
9+
public class MultipartHelper {
10+
11+
private MultipartHelper() {}
12+
13+
/**
14+
* Extracts non-null, non-empty filenames from a collection of multipart {@link Part}s using
15+
* {@link Part#getSubmittedFileName()} (Servlet 3.1+, Jetty 10.0.10+).
16+
*
17+
* @return list of filenames; never {@code null}, may be empty
18+
*/
19+
public static List<String> extractFilenames(Collection<Part> parts) {
20+
if (parts == null || parts.isEmpty()) {
21+
return Collections.emptyList();
22+
}
23+
List<String> filenames = new ArrayList<>();
24+
for (Part part : parts) {
25+
String name = part.getSubmittedFileName();
26+
if (name != null && !name.isEmpty()) {
27+
filenames.add(name);
28+
}
29+
}
30+
return filenames;
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package datadog.trace.instrumentation.jetty1010;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
4+
import static datadog.trace.api.gateway.Events.EVENTS;
5+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
6+
7+
import com.google.auto.service.AutoService;
8+
import datadog.appsec.api.blocking.BlockingException;
9+
import datadog.trace.advice.ActiveRequestContext;
10+
import datadog.trace.advice.RequiresRequestContext;
11+
import datadog.trace.agent.tooling.Instrumenter;
12+
import datadog.trace.agent.tooling.InstrumenterModule;
13+
import datadog.trace.agent.tooling.muzzle.Reference;
14+
import datadog.trace.api.gateway.BlockResponseFunction;
15+
import datadog.trace.api.gateway.CallbackProvider;
16+
import datadog.trace.api.gateway.Flow;
17+
import datadog.trace.api.gateway.RequestContext;
18+
import datadog.trace.api.gateway.RequestContextSlot;
19+
import datadog.trace.bootstrap.CallDepthThreadLocalMap;
20+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
21+
import java.util.Collection;
22+
import java.util.List;
23+
import java.util.function.BiFunction;
24+
import javax.servlet.http.Part;
25+
import net.bytebuddy.asm.Advice;
26+
import net.bytebuddy.implementation.bytecode.assign.Assigner;
27+
import org.eclipse.jetty.server.Request;
28+
import org.eclipse.jetty.util.MultiMap;
29+
30+
@AutoService(InstrumenterModule.class)
31+
public class RequestExtractContentParametersInstrumentation extends InstrumenterModule.AppSec
32+
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
33+
private static final String MULTI_MAP_INTERNAL_NAME = "Lorg/eclipse/jetty/util/MultiMap;";
34+
35+
public RequestExtractContentParametersInstrumentation() {
36+
super("jetty");
37+
}
38+
39+
@Override
40+
public String instrumentedType() {
41+
return "org.eclipse.jetty.server.Request";
42+
}
43+
44+
@Override
45+
public String[] helperClassNames() {
46+
return new String[] {packageName + ".MultipartHelper"};
47+
}
48+
49+
@Override
50+
public void methodAdvice(MethodTransformer transformer) {
51+
transformer.applyAdvice(
52+
named("extractContentParameters").and(takesArguments(0)).or(named("getParts")),
53+
getClass().getName() + "$ExtractContentParametersAdvice");
54+
transformer.applyAdvice(
55+
named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice");
56+
transformer.applyAdvice(
57+
named("getParts").and(takesArguments(1)),
58+
getClass().getName() + "$GetFilenamesFromMultiPartAdvice");
59+
}
60+
61+
// Discriminates Jetty 10.0.10–10.0.x ([10.0.10, 11.0)):
62+
// - _contentParameters + extractContentParameters(void) exist from 9.3+ (excludes 9.2)
63+
// - _multiParts: MultiParts exists in 10.0.10+ (excludes 10.0.0–10.0.9 where it was
64+
// MultiPartFormInputStream, covered by jetty-appsec-10.0)
65+
// - _queryEncoding: Charset exists in all 10.x (excludes 9.4.x where it is String, which also
66+
// has _multiParts: MultiParts from 9.4.10+)
67+
// - javax.servlet.http.Part exists in 10.x classpath (excludes Jetty 11+ which uses jakarta)
68+
private static final Reference REQUEST_REFERENCE =
69+
new Reference.Builder("org.eclipse.jetty.server.Request")
70+
.withMethod(new String[0], 0, "extractContentParameters", "V")
71+
.withField(new String[0], 0, "_contentParameters", MULTI_MAP_INTERNAL_NAME)
72+
.withField(new String[0], 0, "_multiParts", "Lorg/eclipse/jetty/server/MultiParts;")
73+
.withField(new String[0], 0, "_queryEncoding", "Ljava/nio/charset/Charset;")
74+
.build();
75+
76+
private static final Reference JAVAX_PART_REFERENCE =
77+
new Reference.Builder("javax.servlet.http.Part").build();
78+
79+
@Override
80+
public Reference[] additionalMuzzleReferences() {
81+
return new Reference[] {REQUEST_REFERENCE, JAVAX_PART_REFERENCE};
82+
}
83+
84+
@RequiresRequestContext(RequestContextSlot.APPSEC)
85+
public static class ExtractContentParametersAdvice {
86+
@Advice.OnMethodEnter(suppress = Throwable.class)
87+
static boolean before(@Advice.FieldValue("_contentParameters") final MultiMap<String> map) {
88+
final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Request.class);
89+
return callDepth == 0 && map == null;
90+
}
91+
92+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
93+
static void after(
94+
@Advice.Enter boolean proceed,
95+
@Advice.FieldValue("_contentParameters") final MultiMap<String> map,
96+
@ActiveRequestContext RequestContext reqCtx,
97+
@Advice.Thrown(readOnly = false) Throwable t) {
98+
CallDepthThreadLocalMap.decrementCallDepth(Request.class);
99+
if (!proceed) {
100+
return;
101+
}
102+
if (map == null || map.isEmpty()) {
103+
return;
104+
}
105+
106+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
107+
BiFunction<RequestContext, Object, Flow<Void>> callback =
108+
cbp.getCallback(EVENTS.requestBodyProcessed());
109+
if (callback == null) {
110+
return;
111+
}
112+
113+
Flow<Void> flow = callback.apply(reqCtx, map);
114+
Flow.Action action = flow.getAction();
115+
if (action instanceof Flow.Action.RequestBlockingAction) {
116+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
117+
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
118+
if (blockResponseFunction != null) {
119+
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
120+
if (t == null) {
121+
t = new BlockingException("Blocked request (for Request/extractContentParameters)");
122+
reqCtx.getTraceSegment().effectivelyBlocked();
123+
}
124+
}
125+
}
126+
}
127+
}
128+
129+
/**
130+
* Fires the {@code requestFilesFilenames} event when the application calls public {@code
131+
* getParts()}. Guards prevent double-firing:
132+
*
133+
* <ul>
134+
* <li>{@code _contentParameters != null}: set by {@code extractContentParameters()} (the {@code
135+
* getParameterMap()} path); means filenames were already reported via {@code
136+
* GetFilenamesFromMultiPartAdvice}.
137+
* <li>{@code _multiParts != null}: set by the first {@code getParts()} call in Jetty 10.0.10+;
138+
* means filenames were already reported.
139+
* </ul>
140+
*/
141+
@RequiresRequestContext(RequestContextSlot.APPSEC)
142+
public static class GetFilenamesAdvice {
143+
@Advice.OnMethodEnter(suppress = Throwable.class)
144+
static boolean before(
145+
@Advice.FieldValue("_contentParameters") final MultiMap<String> contentParameters,
146+
@Advice.FieldValue(value = "_multiParts", typing = Assigner.Typing.DYNAMIC)
147+
final Object multiParts) {
148+
final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Collection.class);
149+
return callDepth == 0 && contentParameters == null && multiParts == null;
150+
}
151+
152+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
153+
static void after(
154+
@Advice.Enter boolean proceed,
155+
@Advice.Return Collection<Part> parts,
156+
@ActiveRequestContext RequestContext reqCtx,
157+
@Advice.Thrown(readOnly = false) Throwable t) {
158+
CallDepthThreadLocalMap.decrementCallDepth(Collection.class);
159+
if (!proceed || t != null || parts == null || parts.isEmpty()) {
160+
return;
161+
}
162+
List<String> filenames = MultipartHelper.extractFilenames(parts);
163+
if (filenames.isEmpty()) {
164+
return;
165+
}
166+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
167+
BiFunction<RequestContext, List<String>, Flow<Void>> callback =
168+
cbp.getCallback(EVENTS.requestFilesFilenames());
169+
if (callback == null) {
170+
return;
171+
}
172+
Flow<Void> flow = callback.apply(reqCtx, filenames);
173+
Flow.Action action = flow.getAction();
174+
if (action instanceof Flow.Action.RequestBlockingAction) {
175+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
176+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
177+
if (brf != null) {
178+
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
179+
if (t == null) {
180+
t = new BlockingException("Blocked request (multipart file upload)");
181+
reqCtx.getTraceSegment().effectivelyBlocked();
182+
}
183+
}
184+
}
185+
}
186+
}
187+
188+
/**
189+
* Fires the {@code requestFilesFilenames} event when multipart content is parsed via the internal
190+
* {@code getParts(MultiMap)} path triggered by {@code getParameter*()} / {@code
191+
* getParameterMap()} — i.e. when the application never calls public {@code getParts()}.
192+
*/
193+
@RequiresRequestContext(RequestContextSlot.APPSEC)
194+
public static class GetFilenamesFromMultiPartAdvice {
195+
@Advice.OnMethodEnter(suppress = Throwable.class)
196+
static boolean before() {
197+
return CallDepthThreadLocalMap.incrementCallDepth(Collection.class) == 0;
198+
}
199+
200+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
201+
static void after(
202+
@Advice.Enter boolean proceed,
203+
@Advice.Return Collection<Part> parts,
204+
@ActiveRequestContext RequestContext reqCtx,
205+
@Advice.Thrown(readOnly = false) Throwable t) {
206+
CallDepthThreadLocalMap.decrementCallDepth(Collection.class);
207+
if (!proceed || t != null || parts == null || parts.isEmpty()) {
208+
return;
209+
}
210+
List<String> filenames = MultipartHelper.extractFilenames(parts);
211+
if (filenames.isEmpty()) {
212+
return;
213+
}
214+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
215+
BiFunction<RequestContext, List<String>, Flow<Void>> callback =
216+
cbp.getCallback(EVENTS.requestFilesFilenames());
217+
if (callback == null) {
218+
return;
219+
}
220+
Flow<Void> flow = callback.apply(reqCtx, filenames);
221+
Flow.Action action = flow.getAction();
222+
if (action instanceof Flow.Action.RequestBlockingAction) {
223+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
224+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
225+
if (brf != null) {
226+
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
227+
if (t == null) {
228+
t = new BlockingException("Blocked request (multipart file upload)");
229+
reqCtx.getTraceSegment().effectivelyBlocked();
230+
}
231+
}
232+
}
233+
}
234+
}
235+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
muzzle {
2+
pass {
3+
group = 'org.eclipse.jetty'
4+
module = 'jetty-server'
5+
versions = '[10.0,10.0.10)'
6+
assertInverse = true
7+
javaVersion = 11
8+
}
9+
}
10+
11+
apply from: "$rootDir/gradle/java.gradle"
12+
13+
dependencies {
14+
compileOnly(group: 'org.eclipse.jetty', name: 'jetty-server', version: '10.0.0') {
15+
exclude group: 'org.slf4j', module: 'slf4j-api'
16+
}
17+
}
18+
19+
tasks.withType(JavaCompile).configureEach {
20+
configureCompiler(it, 11, JavaVersion.VERSION_1_8)
21+
}
22+
23+
// testing happens in the jetty-* modules
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package datadog.trace.instrumentation.jetty10;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collection;
5+
import java.util.Collections;
6+
import java.util.List;
7+
import javax.servlet.http.Part;
8+
9+
public class MultipartHelper {
10+
11+
private MultipartHelper() {}
12+
13+
/**
14+
* Extracts non-null, non-empty filenames from a collection of multipart {@link Part}s using
15+
* {@link Part#getSubmittedFileName()} (Servlet 3.1+, Jetty 10.x).
16+
*
17+
* @return list of filenames; never {@code null}, may be empty
18+
*/
19+
public static List<String> extractFilenames(Collection<Part> parts) {
20+
if (parts == null || parts.isEmpty()) {
21+
return Collections.emptyList();
22+
}
23+
List<String> filenames = new ArrayList<>();
24+
for (Part part : parts) {
25+
String name = part.getSubmittedFileName();
26+
if (name != null && !name.isEmpty()) {
27+
filenames.add(name);
28+
}
29+
}
30+
return filenames;
31+
}
32+
}

0 commit comments

Comments
 (0)