Skip to content

Commit 1508fef

Browse files
committed
feat(appsec): advise getPart(String) in Jetty 8 to catch single-part uploads
In Jetty 8.x, getPart(String name) calls _multiPartInputStream.getPart(String) directly without delegating to getParts(). Applications that retrieve only one file via getPart() without ever calling getParts() would have their filename event missed. Add GetPartAdvice to cover this path. The charset fix (AI comment 2) was investigated and is not applicable: HTML5 form submissions always use UTF-8 and browsers never include charset= on individual part Content-Type headers, so the existing hardcoded UTF-8 is correct.
1 parent 74948a7 commit 1508fef

1 file changed

Lines changed: 84 additions & 0 deletions

File tree

dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/RequestGetPartsInstrumentation.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
44
import static datadog.trace.api.gateway.Events.EVENTS;
5+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
56
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
67

78
import com.google.auto.service.AutoService;
@@ -20,9 +21,11 @@
2021
import java.io.IOException;
2122
import java.io.InputStream;
2223
import java.util.Collection;
24+
import java.util.Collections;
2325
import java.util.List;
2426
import java.util.Map;
2527
import java.util.function.BiFunction;
28+
import javax.servlet.http.Part;
2629
import net.bytebuddy.asm.Advice;
2730
import net.bytebuddy.jar.asm.ClassReader;
2831
import net.bytebuddy.jar.asm.ClassVisitor;
@@ -52,6 +55,9 @@ public String[] helperClassNames() {
5255
public void methodAdvice(MethodTransformer transformer) {
5356
transformer.applyAdvice(
5457
named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice");
58+
transformer.applyAdvice(
59+
named("getPart").and(takesArguments(1)).and(takesArgument(0, String.class)),
60+
getClass().getName() + "$GetPartAdvice");
5561
}
5662

5763
@Override
@@ -189,4 +195,82 @@ static void after(
189195
}
190196
}
191197
}
198+
199+
/**
200+
* Fires AppSec events for a single-part upload via {@code getPart(String)}, which in Jetty 8.x
201+
* does NOT delegate to {@code getParts()} — it calls {@code
202+
* _multiPartInputStream.getPart(String)} directly. Without this advice, single-file uploads that
203+
* never call the public {@code getParts()} would be missed.
204+
*/
205+
@RequiresRequestContext(RequestContextSlot.APPSEC)
206+
public static class GetPartAdvice {
207+
@Advice.OnMethodEnter(suppress = Throwable.class)
208+
static boolean before() {
209+
return CallDepthThreadLocalMap.incrementCallDepth(Part.class) == 0;
210+
}
211+
212+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
213+
static void after(
214+
@Advice.Enter boolean proceed,
215+
@Advice.Return Part part,
216+
@ActiveRequestContext RequestContext reqCtx,
217+
@Advice.Thrown(readOnly = false) Throwable t) {
218+
CallDepthThreadLocalMap.decrementCallDepth(Part.class);
219+
if (!proceed || t != null || part == null) {
220+
return;
221+
}
222+
223+
Collection<Part> parts = Collections.singletonList(part);
224+
225+
// Fire requestBodyProcessed with form-field name→value (if not a file upload)
226+
Map<String, List<String>> formFields = PartHelper.extractFormFields(parts);
227+
if (!formFields.isEmpty()) {
228+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
229+
BiFunction<RequestContext, Object, Flow<Void>> bodyCallback =
230+
cbp.getCallback(EVENTS.requestBodyProcessed());
231+
if (bodyCallback != null) {
232+
Flow<Void> flow = bodyCallback.apply(reqCtx, formFields);
233+
Flow.Action action = flow.getAction();
234+
if (action instanceof Flow.Action.RequestBlockingAction) {
235+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
236+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
237+
if (brf != null) {
238+
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
239+
if (t == null) {
240+
t = new BlockingException("Blocked request (multipart form field)");
241+
reqCtx.getTraceSegment().effectivelyBlocked();
242+
}
243+
}
244+
}
245+
}
246+
}
247+
248+
if (t != null) {
249+
return;
250+
}
251+
252+
// Fire requestFilesFilenames with file-upload filename (if a file upload)
253+
List<String> filenames = PartHelper.extractFilenames(parts);
254+
if (!filenames.isEmpty()) {
255+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
256+
BiFunction<RequestContext, List<String>, Flow<Void>> filenamesCallback =
257+
cbp.getCallback(EVENTS.requestFilesFilenames());
258+
if (filenamesCallback != null) {
259+
Flow<Void> flow = filenamesCallback.apply(reqCtx, filenames);
260+
Flow.Action action = flow.getAction();
261+
if (action instanceof Flow.Action.RequestBlockingAction) {
262+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
263+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
264+
if (brf != null) {
265+
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
266+
if (t == null) {
267+
t = new BlockingException("Blocked request (multipart file upload)");
268+
reqCtx.getTraceSegment().effectivelyBlocked();
269+
}
270+
}
271+
}
272+
}
273+
}
274+
}
275+
}
192276
}

0 commit comments

Comments
 (0)