Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,10 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
false
}

boolean testBodyFilenamesCalledOnce() {
false
}

boolean testBodyFilenames() {
false
}
Expand Down Expand Up @@ -476,6 +480,7 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
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, '<foo attr="attr_value">mytext<bar/></foo>'),
REDIRECT("redirect", 302, "/redirected"),
Expand Down Expand Up @@ -1646,6 +1651,30 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
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())
Expand Down Expand Up @@ -2581,6 +2610,7 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
boolean responseBodyTag
Object responseBody
List<String> uploadedFilenames
int uploadedFilenamesCallCount = 0
}

static final String stringOrEmpty(String string) {
Expand Down Expand Up @@ -2754,6 +2784,8 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
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<RequestContext, List<String>, Flow<Void>>)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -194,6 +203,79 @@ 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<String> 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;
}
}
}
}
}
if (filenames.isEmpty()) {
return;
}
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
BiFunction<RequestContext, List<String>, Flow<Void>> callback =
cbp.getCallback(EVENTS.requestFilesFilenames());
if (callback == null) {
return;
}
Flow<Void> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,6 +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"), getClass().getName() + "$GetFilenamesAdvice");
}

private static final Reference REQUEST_REFERENCE =
Expand Down Expand Up @@ -135,4 +140,54 @@ static void after(
}
}
}

@RequiresRequestContext(RequestContextSlot.APPSEC)
public static class GetFilenamesAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
static boolean before(@Advice.FieldValue("_contentParameters") final MultiMap<String> 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) {
CallDepthThreadLocalMap.decrementCallDepth(Collection.class);
if (!proceed || t != null || parts == null || parts.isEmpty()) {
return;
}
List<String> 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<RequestContext, List<String>, Flow<Void>> callback =
cbp.getCallback(EVENTS.requestFilesFilenames());
if (callback == null) {
return;
}
Flow<Void> 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();
}
}
}
}
}
}
Loading
Loading