Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9b1e4df
refactor(appsec): extract MultipartContentDecoder to internal-api for…
jandro996 Apr 27, 2026
e051d4b
test(appsec): add missing corner cases to MultipartContentDecoderTest
jandro996 Apr 27, 2026
900984d
fix(appsec): use machine default charset as fallback in MultipartCont…
jandro996 Apr 28, 2026
5f8e0db
test(appsec): migrate MultipartContentDecoderTest from Groovy/Spock t…
jandro996 Apr 28, 2026
823630a
feat(appsec): extend files_content WAF address to Tomcat and Netty mu…
jandro996 Apr 24, 2026
a604480
fix(appsec): address techdebt in Tomcat/Netty files_content instrumen…
jandro996 Apr 24, 2026
ac07eef
fix(appsec): further cleanup of Tomcat/Netty files_content instrument…
jandro996 Apr 24, 2026
3f003e0
fix(appsec): align blocking paths with canonical pattern in Tomcat/Ne…
jandro996 Apr 24, 2026
1357493
fix(appsec): skip file content I/O in Netty when requestFilesContent …
jandro996 Apr 24, 2026
108d3e4
fix(appsec): align Netty filenames blocking and merge CommonsFileUplo…
jandro996 Apr 24, 2026
9bef685
refactor(appsec): remove dead readContents() from FileItemContentReader
jandro996 Apr 24, 2026
d648b6e
fix(appsec): lazy-init filesContent in Netty and align CommonsFileUpl…
jandro996 Apr 24, 2026
a653441
fix(appsec): skip file content I/O in Tomcat when requestFilesContent…
jandro996 Apr 24, 2026
44eddcb
fix(appsec): move BlockingException inside brf guard in all remaining…
jandro996 Apr 24, 2026
9b52458
refactor(appsec): cache reflection and use try-with-resources in Para…
jandro996 Apr 24, 2026
dbbc572
test(appsec): add Netty integration tests for multipart filenames and…
jandro996 Apr 24, 2026
6d847f6
refactor(appsec): extract Netty file content reading to NettyFileUplo…
jandro996 Apr 24, 2026
e811154
test(appsec): add integration tests for file content size and count l…
jandro996 Apr 24, 2026
59fe033
test(appsec): increase Netty test server aggregator limit to support …
jandro996 Apr 24, 2026
95573bb
fix(appsec): do not gate Netty file content callback on requestBodyPr…
jandro996 Apr 24, 2026
48aeb71
style: apply spotless formatting to HttpServerTest
jandro996 Apr 24, 2026
73ffb6e
fix(appsec): remove effectivelyBlocked() calls in Netty filenames and…
jandro996 Apr 24, 2026
3355a15
fix(appsec): set t before effectivelyBlocked() in Tomcat filenames an…
jandro996 Apr 24, 2026
37a5915
perf(appsec): skip attribute I/O in Netty multipart when body callbac…
jandro996 Apr 24, 2026
d1573f8
refactor(appsec): unify file content limits via Config for DD_APPSEC_…
jandro996 Apr 27, 2026
0d0e2f0
chore: register DD_APPSEC_MAX_FILE_CONTENT_BYTES/COUNT in supported-c…
jandro996 Apr 27, 2026
3c9709a
test(appsec): use Config values instead of hardcoded literals in Para…
jandro996 Apr 27, 2026
abe3cbc
fix(appsec): move MAX_FILES_TO_INSPECT out of advice inner class to f…
jandro996 Apr 27, 2026
2bf3304
style: apply spotless formatting to HttpPostRequestDecoderInstrumenta…
jandro996 Apr 27, 2026
b4d0e6e
fix(appsec): include filenames callback in Netty multipart early-retu…
jandro996 Apr 27, 2026
92d23d9
fix(appsec): use per-part charset for files_content in Tomcat and Netty
jandro996 Apr 28, 2026
a6d638b
refactor(appsec): extract shared InputStream read loop into Multipart…
jandro996 Apr 28, 2026
314867e
test(appsec): add charset-aware decoding tests for readInputStream, N…
jandro996 Apr 28, 2026
bb9cc20
test(appsec): add getContentType() to Tomcat test part doubles to fix…
jandro996 Apr 28, 2026
a0fb59e
perf(appsec): skip filenames list allocation in CommonsFileUpload whe…
jandro996 Apr 28, 2026
fe101c7
refactor(appsec): extract Netty multipart data collection to NettyMul…
jandro996 Apr 28, 2026
6b65fc0
test(appsec): declare ISO-8859-1 content type on RawBytesPart to matc…
jandro996 Apr 28, 2026
a30ea10
refactor(appsec): extract tryBlock helper to NettyMultipartHelper
jandro996 Apr 28, 2026
3d73c77
test(appsec): add unit tests for NettyMultipartHelper.tryBlock
jandro996 Apr 28, 2026
05ba4ba
refactor(appsec): move MAX_FILES_TO_INSPECT to FileItemContentReader …
jandro996 Apr 29, 2026
a40ec38
refactor(appsec): replace Map.Entry method cache with four flat volat…
jandro996 Apr 29, 2026
eef2b0a
test(appsec): add unit tests for FileItemContentReader.addToContents(…
jandro996 Apr 29, 2026
fc8a83a
fix(appsec): preserve decoder exception when body callback does not b…
jandro996 Apr 29, 2026
c426af2
fix(appsec): make Tomcat Part method cache atomic with an immutable h…
jandro996 Apr 29, 2026
64dd1f5
fix(appsec): inject CachedMethods helper class in Tomcat ParsePartsIn…
jandro996 Apr 29, 2026
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 @@ -136,6 +136,7 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
ss.registerCallback(events.requestBodyDone(), callbacks.requestBodyEndCb)
ss.registerCallback(events.requestBodyProcessed(), callbacks.requestBodyObjectCb)
ss.registerCallback(events.requestFilesFilenames(), callbacks.requestFilesFilenamesCb)
ss.registerCallback(events.requestFilesContent(), callbacks.requestFilesContentCb)
ss.registerCallback(events.responseBody(), callbacks.responseBodyObjectCb)
ss.registerCallback(events.responseStarted(), callbacks.responseStartedCb)
ss.registerCallback(events.responseHeader(), callbacks.responseHeaderCb)
Expand Down Expand Up @@ -372,6 +373,10 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
false
}

boolean testBodyFilesContent() {
false
}

boolean testBodyJson() {
false
}
Expand Down Expand Up @@ -1652,6 +1657,82 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
response.close()
}

def 'test instrumentation gateway file upload content'() {
setup:
assumeTrue(testBodyFilesContent())
RequestBody fileBody = RequestBody.create(MediaType.parse('application/octet-stream'), 'file content')
def body = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart('file', 'test.bin', fileBody)
.build()
def httpRequest = request(BODY_MULTIPART, 'POST', body).build()
def response = client.newCall(httpRequest).execute()

when:
TEST_WRITER.waitForTraces(1)

then:
TEST_WRITER.get(0).any {
it.getTag('request.body.files_content') == '[file content]'
}

cleanup:
response.close()
}

def 'test instrumentation gateway file upload content truncated at max size'() {
setup:
assumeTrue(testBodyFilesContent())
def maxContentBytes = Config.get().getAppSecMaxFileContentBytes()
def body = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart('file', 'large.bin',
RequestBody.create(MediaType.parse('application/octet-stream'), 'X' * (maxContentBytes + 500)))
.build()
def httpRequest = request(BODY_MULTIPART, 'POST', body).build()
def response = client.newCall(httpRequest).execute()

when:
TEST_WRITER.waitForTraces(1)

then:
TEST_WRITER.get(0).any {
span ->
span.getTag('request.body.files_content') == '[' + 'X' * maxContentBytes + ']'
}

cleanup:
response.close()
}

def 'test instrumentation gateway file upload content max files limit'() {
setup:
assumeTrue(testBodyFilesContent())
def maxFilesToInspect = Config.get().getAppSecMaxFileContentCount()
def bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM)
(1..maxFilesToInspect + 1).each {
i ->
bodyBuilder.addFormDataPart("file$i", "file${i}.bin",
RequestBody.create(MediaType.parse('application/octet-stream'), "content_of_file_$i"))
}
def httpRequest = request(BODY_MULTIPART, 'POST', bodyBuilder.build()).build()
def response = client.newCall(httpRequest).execute()

when:
TEST_WRITER.waitForTraces(1)

then:
TEST_WRITER.get(0).any {
span ->
def tag = span.getTag('request.body.files_content') as String
tag?.contains("content_of_file_$maxFilesToInspect") &&
!tag.contains("content_of_file_${maxFilesToInspect + 1}")
}

cleanup:
response.close()
}

def 'test instrumentation gateway json request body'() {
setup:
assumeTrue(testBodyJson())
Expand Down Expand Up @@ -2589,6 +2670,7 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
boolean responseBodyTag
Object responseBody
List<String> uploadedFilenames
List<String> uploadedFilesContent
}

static final String stringOrEmpty(String string) {
Expand Down Expand Up @@ -2765,6 +2847,15 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
Flow.ResultFlow.empty()
} as BiFunction<RequestContext, List<String>, Flow<Void>>)

final BiFunction<RequestContext, List<String>, Flow<Void>> requestFilesContentCb =
({
RequestContext rqCtxt, List<String> contents ->
rqCtxt.traceSegment.setTagTop('request.body.files_content', contents as String)
Context context = rqCtxt.getData(RequestContextSlot.APPSEC)
context.uploadedFilesContent = contents
Flow.ResultFlow.empty()
} as BiFunction<RequestContext, List<String>, Flow<Void>>)

final BiFunction<RequestContext, Object, Flow<Void>> responseBodyObjectCb =
({
RequestContext rqCtxt, Object obj ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
@AutoService(InstrumenterModule.class)
public class CommonsFileUploadAppSecInstrumentation extends InstrumenterModule.AppSec
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {

public CommonsFileUploadAppSecInstrumentation() {
super("commons-fileupload");
}
Expand Down Expand Up @@ -54,7 +53,6 @@ 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 @@ -73,22 +71,22 @@ static void after(
return;
}

List<String> filenames = new ArrayList<>();
List<String> filenames = filenamesCallback != null ? new ArrayList<>() : null;
List<String> filesContent = contentCallback != null ? new ArrayList<>() : null;
for (FileItem fileItem : fileItems) {
if (fileItem.isFormField()) {
continue;
}
String name = fileItem.getName();
if (name != null && !name.isEmpty()) {
if (filenames != null && name != null && !name.isEmpty()) {
filenames.add(name);
}
}
if (filenames.isEmpty() && contentCallback == null) {
return;
if (filesContent != null) {
FileItemContentReader.addToContents(fileItem, filesContent);
}
}

// Fire filenames event
if (filenamesCallback != null && !filenames.isEmpty()) {
if (filenames != null && !filenames.isEmpty()) {
Flow<Void> flow = filenamesCallback.apply(reqCtx, filenames);
Flow.Action action = flow.getAction();
if (action instanceof Flow.Action.RequestBlockingAction) {
Expand All @@ -98,28 +96,21 @@ static void after(
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 content)");
reqCtx.getTraceSegment().effectivelyBlocked();
if (t == null && filesContent != null && !filesContent.isEmpty()) {
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 content)");
reqCtx.getTraceSegment().effectivelyBlocked();
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,31 @@
package datadog.trace.instrumentation.commons.fileupload;

import datadog.trace.api.Config;
import datadog.trace.api.http.MultipartContentDecoder;
import java.io.IOException;
import java.io.InputStream;
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 final int MAX_CONTENT_BYTES = Config.get().getAppSecMaxFileContentBytes();
public static final int MAX_FILES_TO_INSPECT = Config.get().getAppSecMaxFileContentCount();

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 MultipartContentDecoder.decodeBytes(buf, total, fileItem.getContentType());
return MultipartContentDecoder.readInputStream(
is, MAX_CONTENT_BYTES, fileItem.getContentType());
} catch (IOException ignored) {
return "";
}
}

public static void addToContents(FileItem fileItem, List<String> contents) {
if (contents.size() < MAX_FILES_TO_INSPECT) {
contents.add(readContent(fileItem));
}
}

private FileItemContentReader() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,61 +48,41 @@ class FileItemContentReaderTest extends Specification {
FileItemContentReader.readContent(item) == text
}

void 'readContents returns content for each non-form file with a name'() {
void 'addToContents adds content when below MAX_FILES_TO_INSPECT'() {
given:
def items = [fileItem('content-a', 'file-a.txt'), fileItem('content-b', 'file-b.txt'),]
def contents = []
def item = fileItem('hello')

when:
def result = FileItemContentReader.readContents(items)
FileItemContentReader.addToContents(item, contents)

then:
result == ['content-a', 'content-b']
contents == ['hello']
}

void 'readContents skips form fields'() {
void 'addToContents does not add when at MAX_FILES_TO_INSPECT limit'() {
given:
FileItem formField = Stub(FileItem)
formField.isFormField() >> true
def items = [formField, fileItem('content', 'real.txt')]
def contents = (1..FileItemContentReader.MAX_FILES_TO_INSPECT).collect { "content-${it}" }
def item = fileItem('extra')

when:
def result = FileItemContentReader.readContents(items)
FileItemContentReader.addToContents(item, contents)

then:
result == ['content']
contents.size() == FileItemContentReader.MAX_FILES_TO_INSPECT
}

void 'readContents includes file parts with empty or null name'() {
void 'addToContents fills up to exactly MAX_FILES_TO_INSPECT'() {
given:
def items = [
fileItem('content-no-name', null),
fileItem('content-empty-name', ''),
fileItem('content-named', 'named.txt'),
]
def contents = (1..<FileItemContentReader.MAX_FILES_TO_INSPECT).collect { "content-${it}" }
def item = fileItem('last')

when:
def result = FileItemContentReader.readContents(items)
FileItemContentReader.addToContents(item, contents)

then:
result == ['content-no-name', 'content-empty-name', 'content-named']
}

void 'readContents stops after MAX_FILES_TO_INSPECT files'() {
given:
def items = (1..FileItemContentReader.MAX_FILES_TO_INSPECT + 1).collect {
fileItem("content-${it}", "file-${it}.txt")
}

when:
def result = FileItemContentReader.readContents(items)

then:
result.size() == FileItemContentReader.MAX_FILES_TO_INSPECT
}

void 'readContents returns empty list for empty input'() {
expect:
FileItemContentReader.readContents([]) == []
contents.size() == FileItemContentReader.MAX_FILES_TO_INSPECT
contents.last() == 'last'
}

private FileItem fileItem(String content) {
Expand Down
Loading
Loading