Skip to content
Merged
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 @@ -74,6 +74,13 @@ public interface KnownAddresses {
Address<Long> REQUEST_COMBINED_FILE_SIZE =
new Address<>("server.request.body.combined_file_size");

/**
* Contains the content of each uploaded file in a multipart/form-data request. Content is
* truncated per file and only the first files are inspected. Available only on inspected
* multipart/form-data requests.
*/
Address<List<String>> REQUEST_FILES_CONTENT = new Address<>("server.request.body.files_content");

/**
* The parsed query string.
*
Expand Down Expand Up @@ -205,6 +212,8 @@ static Address<?> forName(String name) {
return REQUEST_FILES_FILENAMES;
case "server.request.body.combined_file_size":
return REQUEST_COMBINED_FILE_SIZE;
case "server.request.body.files_content":
return REQUEST_FILES_CONTENT;
case "server.request.query":
return REQUEST_QUERY;
case "server.request.headers.no_cookies":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ public class GatewayBridge {
private volatile DataSubscriberInfo execCmdSubInfo;
private volatile DataSubscriberInfo shellCmdSubInfo;
private volatile DataSubscriberInfo requestFilesFilenamesSubInfo;
private volatile DataSubscriberInfo requestFilesContentSubInfo;

public GatewayBridge(
SubscriptionService subscriptionService,
Expand Down Expand Up @@ -208,6 +209,10 @@ public void init() {
subscriptionService.registerCallback(
EVENTS.requestFilesFilenames(), this::onRequestFilesFilenames);
}
if (additionalIGEvents.contains(EVENTS.requestFilesContent())) {
subscriptionService.registerCallback(
EVENTS.requestFilesContent(), this::onRequestFilesContent);
}
}

/**
Expand Down Expand Up @@ -235,6 +240,7 @@ public void reset() {
execCmdSubInfo = null;
shellCmdSubInfo = null;
requestFilesFilenamesSubInfo = null;
requestFilesContentSubInfo = null;
}

private Flow<Void> onUser(final RequestContext ctx_, final String user) {
Expand Down Expand Up @@ -605,6 +611,31 @@ private Flow<Void> onRequestFilesFilenames(RequestContext ctx_, List<String> fil
}
}

private Flow<Void> onRequestFilesContent(RequestContext ctx_, List<String> filesContent) {
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
if (ctx == null || filesContent == null || filesContent.isEmpty()) {
return NoopFlow.INSTANCE;
}
while (true) {
DataSubscriberInfo subInfo = requestFilesContentSubInfo;
if (subInfo == null) {
subInfo = producerService.getDataSubscribers(KnownAddresses.REQUEST_FILES_CONTENT);
requestFilesContentSubInfo = subInfo;
}
if (subInfo == null || subInfo.isEmpty()) {
return NoopFlow.INSTANCE;
}
DataBundle bundle =
new SingletonDataBundle<>(KnownAddresses.REQUEST_FILES_CONTENT, filesContent);
try {
GatewayContext gwCtx = new GatewayContext(false);
return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx);
} catch (ExpiredSubscriberInfoException e) {
requestFilesContentSubInfo = null;
}
}
}

private Flow<Void> onDatabaseSqlQuery(RequestContext ctx_, String sql) {
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
if (ctx == null) {
Expand Down Expand Up @@ -1464,6 +1495,7 @@ private static class IGAppSecEventDependencies {
DATA_DEPENDENCIES.put(KnownAddresses.REQUEST_BODY_OBJECT, l(EVENTS.requestBodyProcessed()));
DATA_DEPENDENCIES.put(
KnownAddresses.REQUEST_FILES_FILENAMES, l(EVENTS.requestFilesFilenames()));
DATA_DEPENDENCIES.put(KnownAddresses.REQUEST_FILES_CONTENT, l(EVENTS.requestFilesContent()));
}

private static Collection<datadog.trace.api.gateway.EventType<?>> l(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class KnownAddressesSpecificationForkedTest extends Specification {
'server.request.body.files_field_names',
'server.request.body.filenames',
'server.request.body.combined_file_size',
'server.request.body.files_content',
'server.request.query',
'server.request.headers.no_cookies',
'grpc.server.method',
Expand Down Expand Up @@ -58,7 +59,7 @@ class KnownAddressesSpecificationForkedTest extends Specification {

void 'number of known addresses is expected number'() {
expect:
Address.instanceCount() == 46
Address.instanceCount() == 47
KnownAddresses.WAF_CONTEXT_PROCESSOR.serial == Address.instanceCount() - 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,15 @@ class GatewayBridgeIGRegistrationSpecification extends DDSpecification {
then:
1 * ig.registerCallback(Events.REQUEST_BODY_DONE, _)
}

void 'requestFilesContent is registered via data address'() {
given:
1 * eventDispatcher.allSubscribedDataAddresses() >> [KnownAddresses.REQUEST_FILES_CONTENT]

when:
bridge.init()

then:
1 * ig.registerCallback(Events.get().requestFilesContent(), _)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class GatewayBridgeSpecification extends DDSpecification {
BiFunction<RequestContext, String, Flow<Void>> fileLoadedCB
BiFunction<RequestContext, String, Flow<Void>> fileWrittenCB
BiFunction<RequestContext, List<String>, Flow<Void>> requestFilesFilenamesCB
BiFunction<RequestContext, List<String>, Flow<Void>> requestFilesContentCB
BiFunction<RequestContext, String, Flow<Void>> requestSessionCB
BiFunction<RequestContext, String[], Flow<Void>> execCmdCB
BiFunction<RequestContext, String, Flow<Void>> shellCmdCB
Expand Down Expand Up @@ -463,7 +464,7 @@ class GatewayBridgeSpecification extends DDSpecification {

void callInitAndCaptureCBs() {
// force all callbacks to be registered
_ * eventDispatcher.allSubscribedDataAddresses() >> [KnownAddresses.REQUEST_PATH_PARAMS, KnownAddresses.REQUEST_BODY_OBJECT, KnownAddresses.REQUEST_FILES_FILENAMES]
_ * eventDispatcher.allSubscribedDataAddresses() >> [KnownAddresses.REQUEST_PATH_PARAMS, KnownAddresses.REQUEST_BODY_OBJECT, KnownAddresses.REQUEST_FILES_FILENAMES, KnownAddresses.REQUEST_FILES_CONTENT]

1 * ig.registerCallback(EVENTS.requestStarted(), _) >> {
requestStartedCB = it[1]; null
Expand Down Expand Up @@ -561,6 +562,9 @@ class GatewayBridgeSpecification extends DDSpecification {
1 * ig.registerCallback(EVENTS.requestFilesFilenames(), _) >> {
requestFilesFilenamesCB = it[1]; null
}
1 * ig.registerCallback(EVENTS.requestFilesContent(), _) >> {
requestFilesContentCB = it[1]; null
}
0 * ig.registerCallback(_, _)

bridge.init()
Expand Down Expand Up @@ -1142,6 +1146,38 @@ class GatewayBridgeSpecification extends DDSpecification {
0 * eventDispatcher.publishDataEvent(*_)
}

void 'process request files content'() {
setup:
final filesContent = ['%PDF-1.4 malicious content', '#!/bin/bash\nrm -rf /']
eventDispatcher.getDataSubscribers({
KnownAddresses.REQUEST_FILES_CONTENT in it
}) >> nonEmptyDsInfo
DataBundle bundle
GatewayContext gatewayContext

when:
Flow<?> flow = requestFilesContentCB.apply(ctx, filesContent)

then:
1 * eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> {
a, b, db, gw -> bundle = db; gatewayContext = gw; NoopFlow.INSTANCE
}
bundle.get(KnownAddresses.REQUEST_FILES_CONTENT) == filesContent
flow.result == null
flow.action == Flow.Action.Noop.INSTANCE
gatewayContext.isTransient == false
gatewayContext.isRasp == false
}

void 'process request files content with empty list returns noop'() {
when:
Flow<?> flow = requestFilesContentCB.apply(ctx, [])

then:
flow == NoopFlow.INSTANCE
0 * eventDispatcher.publishDataEvent(*_)
}

void 'process exec cmd'() {
setup:
final cmd = ['/bin/../usr/bin/reboot', '-f'] as String[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
import org.apache.commons.fileupload.FileItem;

@AutoService(InstrumenterModule.class)
public class CommonsFileUploadAppSecModule extends InstrumenterModule.AppSec
public class CommonsFileUploadAppSecInstrumentation extends InstrumenterModule.AppSec
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {

public CommonsFileUploadAppSecModule() {
public CommonsFileUploadAppSecInstrumentation() {
super("commons-fileupload");
}

Expand All @@ -36,6 +36,13 @@ public String instrumentedType() {
return "org.apache.commons.fileupload.servlet.ServletFileUpload";
}

@Override
public String[] helperClassNames() {
return new String[] {
"datadog.trace.instrumentation.commons.fileupload.FileItemContentReader",
};
}

@Override
public void methodAdvice(MethodTransformer transformer) {
transformer.applyAdvice(
Expand All @@ -47,6 +54,7 @@ public void methodAdvice(MethodTransformer transformer) {

@RequiresRequestContext(RequestContextSlot.APPSEC)
public static class ParseRequestAdvice {

@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
static void after(
@Advice.Return final List<FileItem> fileItems,
Expand All @@ -57,9 +65,11 @@ static void after(
}

CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
BiFunction<RequestContext, List<String>, Flow<Void>> callback =
BiFunction<RequestContext, List<String>, Flow<Void>> filenamesCallback =
cbp.getCallback(EVENTS.requestFilesFilenames());
if (callback == null) {
BiFunction<RequestContext, List<String>, Flow<Void>> contentCallback =
cbp.getCallback(EVENTS.requestFilesContent());
if (filenamesCallback == null && contentCallback == null) {
return;
}

Expand All @@ -73,18 +83,42 @@ static void after(
filenames.add(name);
}
}
if (filenames.isEmpty()) {
if (filenames.isEmpty() && contentCallback == 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;
// Fire filenames event
if (filenamesCallback != null && !filenames.isEmpty()) {
Flow<Void> flow = filenamesCallback.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);
t = new BlockingException("Blocked request (multipart file upload)");
reqCtx.getTraceSegment().effectivelyBlocked();
return;
}
}
}

// Fire content event only if not blocked
if (contentCallback == null) {
return;
}
List<String> filesContent = FileItemContentReader.readContents(fileItems);
if (filesContent.isEmpty()) {
return;
}
Flow<Void> contentFlow = contentCallback.apply(reqCtx, filesContent);
Flow.Action contentAction = contentFlow.getAction();
if (contentAction instanceof Flow.Action.RequestBlockingAction) {
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) contentAction;
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
if (brf != null) {
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
t = new BlockingException("Blocked request (multipart file upload)");
t = new BlockingException("Blocked request (multipart file upload content)");
reqCtx.getTraceSegment().effectivelyBlocked();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package datadog.trace.instrumentation.commons.fileupload;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.fileupload.FileItem;

/** Reads uploaded file content for WAF inspection. */
public final class FileItemContentReader {
public static final int MAX_CONTENT_BYTES = 4096;
public static final int MAX_FILES_TO_INSPECT = 25;

public static List<String> readContents(List<FileItem> fileItems) {
List<String> result = new ArrayList<>();
for (FileItem fileItem : fileItems) {
if (result.size() >= MAX_FILES_TO_INSPECT) {
break;
}
if (fileItem.isFormField()) {
continue;
}
result.add(readContent(fileItem));
}
return result;
}

public static String readContent(FileItem fileItem) {
try (InputStream is = fileItem.getInputStream()) {
byte[] buf = new byte[MAX_CONTENT_BYTES];
int total = 0;
int n;
while (total < MAX_CONTENT_BYTES
&& (n = is.read(buf, total, MAX_CONTENT_BYTES - total)) != -1) {
total += n;
}
return new String(buf, 0, total, StandardCharsets.ISO_8859_1);
} catch (IOException ignored) {
return "";
}
}

private FileItemContentReader() {}
}
Loading
Loading