Skip to content

Commit 35ed9fb

Browse files
authored
Add server.request.body.files_content AppSec address for Tomcat and Netty multipart (#11198)
refactor(appsec): extract MultipartContentDecoder to internal-api for reuse across integrations test(appsec): add missing corner cases to MultipartContentDecoderTest fix(appsec): use machine default charset as fallback in MultipartContentDecoder Replaces hardcoded UTF-8 (no-charset default) and ISO-8859-1 (fallback) with Charset.defaultCharset() in both cases, per reviewer feedback. test(appsec): migrate MultipartContentDecoderTest from Groovy/Spock to Java/JUnit 5 feat(appsec): extend files_content WAF address to Tomcat and Netty multipart Read up to 4096 bytes per file (ISO-8859-1, max 25 files) and fire the server.request.body.files_content callback after the filenames callback, only when the request has not already been blocked. - ParameterCollector: add readContent() via reflection on getInputStream(), getContents() getter, and MAX_FILES_TO_INSPECT guard - ParsePartsInstrumentation: dispatch requestFilesContent() after requestFilesFilenames() when t == null - HttpPostRequestDecoderInstrumentation: collect file content inline in the getBodyHttpDatas() loop (in-memory via ByteBuf, on-disk via FileInputStream) and dispatch requestFilesContent() when thr == null after filenames - Add ParameterCollectorImplTest covering truncation, 25-file cap, form-field skip, IOException fallback, and ISO-8859-1 round-trip fix(appsec): address techdebt in Tomcat/Netty files_content instrumentation Issue 1 — Netty: extract MAX_CONTENT_BYTES=4096 and MAX_FILES_TO_INSPECT=25 as named constants in ParseBodyAdvice, replacing raw magic literals. Issue 3 — Tomcat + Netty: call effectivelyBlocked() after tryCommitBlockingResponse in both the filenames and files_content blocking paths, matching the behaviour already present in CommonsFileUploadAppSecInstrumentation. Issue 4 — Netty: collect file content for ALL FileUpload data items regardless of whether filename is present or empty; HttpDataType.FileUpload is the file-vs-form-field discriminator in Netty's model, so the filename-empty check was incorrectly skipping content for unnamed file parts (inconsistent with FileItemContentReader which only skips isFormField() items). fix(appsec): further cleanup of Tomcat/Netty files_content instrumentation Issue 1 — Tomcat: inspect file content even when submitted filename is empty. getSubmittedFileName() returns null for form fields (no filename parameter) and "" for file uploads whose client explicitly sent filename="". Null is now the only skip condition, so file parts with an empty filename are inspected but not added to the filenames list, matching the commons-fileupload and Netty behaviour. Issue 2 — Netty: remove unnecessary (long) casts in Math.min; both operands are already int so Math.min(int,int) resolves directly. Issue 3 — Netty: replace manual try/finally on FileInputStream with try-with-resources, consistent with FileItemContentReader style. fix(appsec): align blocking paths with canonical pattern in Tomcat/Netty files_content Move BlockingException assignment inside the brf != null guard so it is only thrown when the blocking response was actually committed. Also add effectivelyBlocked() to the Tomcat body-processed path, and remove the redundant if (t == null) guard there. For the Netty body-processed path (urlencoded), effectivelyBlocked() is intentionally omitted: tryCommitBlockingResponse() finishes the Netty span synchronously, so calling effectivelyBlocked() afterwards would interact with an already-closed TraceSegment. fix(appsec): skip file content I/O in Netty when requestFilesContent callback is unregistered Pre-fetch the requestFilesContent callback before the body-data loop and guard ByteBuf/FileInputStream reads behind it, consistent with how CommonsFileUploadAppSecInstrumentation already does. Also flattens the post-loop dispatch block by one nesting level. fix(appsec): align Netty filenames blocking and merge CommonsFileUpload loop Move thr = new BlockingException(...) for the filenames path in Netty inside the brf != null guard, consistent with the body-processed and content paths in the same advice and with the Tomcat/Liberty pattern. Merge the two FileItem iterations in CommonsFileUploadAppSecInstrumentation into a single loop that builds both filenames and filesContent in one pass, calling FileItemContentReader.readContent() per item instead of a separate FileItemContentReader.readContents() sweep. refactor(appsec): remove dead readContents() from FileItemContentReader The bulk readContents(List<FileItem>) method is no longer called — the advice iterates file items inline calling readContent(FileItem) per item. Remove the method, its now-unused ArrayList/List imports, and the corresponding test cases. Also fix redundant type witness on ArrayList construction in CommonsFileUploadAppSecInstrumentation. fix(appsec): lazy-init filesContent in Netty and align CommonsFileUpload guard style Skip filesContent ArrayList allocation in Netty ParseBodyAdvice when the requestFilesContent callback is not registered, matching the null-sentinel pattern already used in CommonsFileUploadAppSecInstrumentation. Replace the early-return in CommonsFileUploadAppSecInstrumentation's filenames blocking branch with a t == null guard on the content dispatch block, aligning control flow with ParsePartsInstrumentation and HttpPostRequestDecoderInstrumentation. fix(appsec): skip file content I/O in Tomcat when requestFilesContent callback is unregistered Add an inspectContent boolean to ParameterCollectorImpl, set from the presence of the requestFilesContent callback in ParsePartsInstrumentation before(). When false, addPart() still collects filenames but skips the getInputStream() read, matching the lazy-init approach already used by the Netty and commons-fileupload integrations. fix(appsec): move BlockingException inside brf guard in all remaining paths The content path in Netty and both the filenames and content paths in Tomcat were setting the BlockingException outside the if (brf != null) block, meaning the exception would be thrown even when no blocking response had actually been committed. Align all three with the canonical pattern: tryCommitBlockingResponse + effectivelyBlocked + exception assignment all inside if (brf != null). The Netty body-processed path intentionally still omits effectivelyBlocked() because tryCommitBlockingResponse() closes the Netty span synchronously in the test environment, making a subsequent effectivelyBlocked() call fail. refactor(appsec): cache reflection and use try-with-resources in ParameterCollector - Replace per-call getMethod() with volatile (Class, Method) entry cache so reflection resolves once per concrete Part class rather than per file per request. - Use try-with-resources for InputStream in readContent() instead of try/finally. - Add unit test covering the Tomcat 7 getFilename() fallback path. test(appsec): add Netty integration tests for multipart filenames and file content Also fix HttpPostRequestDecoderInstrumentation to fire on PREEPILOGUE+isLastChunk so that multipart requests delivered as a FullHttpRequest (single shot) trigger the requestBodyProcessed/requestFilesFilenames/requestFilesContent callbacks. The PREEPILOGUE→EPILOGUE transition requires a second parseBody() call which never comes when the full request arrives in one shot; PREEPILOGUE+isLastChunk is equivalent to EPILOGUE in that scenario. refactor(appsec): extract Netty file content reading to NettyFileUploadContentReader helper Move the in-memory/disk-backed FileUpload content reading logic out of ParseBodyAdvice into a testable static helper class. Also fixes the partial-read risk on disk-backed uploads (single fis.read() replaced by a loop) and broadens the catch to Exception to handle IllegalReferenceCountException from released ByteBufs. test(appsec): add integration tests for file content size and count limits test(appsec): increase Netty test server aggregator limit to support large multipart payloads fix(appsec): do not gate Netty file content callback on requestBodyProcessed subscriber style: apply spotless formatting to HttpServerTest fix(appsec): remove effectivelyBlocked() calls in Netty filenames and content blocking paths fix(appsec): set t before effectivelyBlocked() in Tomcat filenames and content blocking paths perf(appsec): skip attribute I/O in Netty multipart when body callback is absent When only requestFilesContent is registered, avoid calling getValue() on form attributes (which may trigger disk reads for large fields). refactor(appsec): unify file content limits via Config for DD_APPSEC_MAX_FILE_CONTENT_BYTES/COUNT Replace hardcoded constants MAX_CONTENT_BYTES=4096 and MAX_FILES_TO_INSPECT=25 (duplicated across commons-fileupload, Netty, and Tomcat) with two Config-backed variables: appsec.max.file-content.bytes and appsec.max.file-content.count. chore: register DD_APPSEC_MAX_FILE_CONTENT_BYTES/COUNT in supported-configurations.json test(appsec): use Config values instead of hardcoded literals in ParameterCollectorImplTest fix(appsec): move MAX_FILES_TO_INSPECT out of advice inner class to fix muzzle validation Static fields in @RequiresRequestContext advice inner classes trigger muzzle to treat the advice class itself as a user-classpath class; moving the constant to NettyFileUploadContentReader (a helper) avoids the self-referential muzzle failure. style: apply spotless formatting to HttpPostRequestDecoderInstrumentation fix(appsec): include filenames callback in Netty multipart early-return gate requestFilesFilenames() was consulted after data collection, so when it was the only registered AppSec callback (no requestBodyProcessed, no requestFilesContent) the early-return gate fired and filenames were never published to the WAF. Fetch all three callbacks upfront and gate on all three. fix(appsec): use per-part charset for files_content in Tomcat and Netty Introduces MultipartContentDecoder (internal-api) to decode multipart file bytes using the charset declared in each part's Content-Type header, with JVM-default fallback and REPLACE on malformed input. Mirrors the approach in PR #11212 for commons-fileupload so all three integrations share the same decoding logic. refactor(appsec): extract shared InputStream read loop into MultipartContentDecoder Add readInputStream(InputStream, int, String) to eliminate the identical bounded read loop duplicated across FileItemContentReader, NettyFileUploadContentReader and ParameterCollector. Each caller still owns its own MAX_CONTENT_BYTES constant. Also consolidate the two volatile reflection caches in ParameterCollector.readContent (getInputStream + getContentType) into a single Method[] entry to halve volatile reads per invocation. test(appsec): add charset-aware decoding tests for readInputStream, Netty and Tomcat helpers test(appsec): add getContentType() to Tomcat test part doubles to fix reflective method resolution perf(appsec): skip filenames list allocation in CommonsFileUpload when filenames callback is absent refactor(appsec): extract Netty multipart data collection to NettyMultipartHelper and add unit tests test(appsec): declare ISO-8859-1 content type on RawBytesPart to match decoder charset expectation refactor(appsec): extract tryBlock helper to NettyMultipartHelper Deduplicates the identical RequestBlockingAction dispatch pattern that appeared three times in the Netty multipart advice. test(appsec): add unit tests for NettyMultipartHelper.tryBlock refactor(appsec): move MAX_FILES_TO_INSPECT to FileItemContentReader helper with addToContents() The constant now lives in the helper class (listed in helperClassNames()) where it is actually used, instead of the outer instrumentation class which is not visible from inlined advice bytecode in the app classloader. addToContents() encapsulates the limit check alongside the constant. refactor(appsec): replace Map.Entry method cache with four flat volatile fields in ParameterCollector Use separate volatile fields (cachedPartClass, cachedGetInputStream, cachedGetContentType, cachedGetFilename) instead of Map.Entry<Class<?>, Method[]> + Map.Entry<Class<?>, Method>. cachedPartClass is written last to safely publish the three methods per JMM volatile ordering. test(appsec): add unit tests for FileItemContentReader.addToContents() limit behaviour fix(appsec): preserve decoder exception when body callback does not block in Netty multipart Unconditionally assigning thr = tryBlock(...) would null out an original decoder exception (e.g. malformed multipart on an over-limit body) when tryBlock returns null for a non-blocking flow. Only overwrite thr when tryBlock returns a blocking exception. fix(appsec): make Tomcat Part method cache atomic with an immutable holder Four flat volatile fields have a TOCTOU race: thread A passes the cachedPartClass check, thread B overwrites the three method fields for a different class, and thread A invokes the wrong Method on its instance (IllegalArgumentException is swallowed, the part is silently dropped). Replace the four fields with a single volatile CachedMethods reference that holds an immutable snapshot of all three methods. Readers load the reference once into a local variable, so their snapshot stays consistent even if another thread publishes a new entry concurrently. Add a concurrency regression test that runs two threads simultaneously with different concrete Part classes; it detects dropped parts caused by the race. fix(appsec): inject CachedMethods helper class in Tomcat ParsePartsInstrumentation ByteBuddy only injects classes explicitly listed in helperClassNames(); nested inner classes are not auto-discovered. Without this entry, the first addPart() call triggers NoClassDefFoundError on CachedMethods, which is swallowed by the broad catch in addPart(), silently dropping all filenames and file contents. Co-authored-by: alejandro.gonzalez <alejandro.gonzalez@datadoghq.com>
1 parent 9cb85ec commit 35ed9fb

17 files changed

Lines changed: 1366 additions & 158 deletions

File tree

dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
136136
ss.registerCallback(events.requestBodyDone(), callbacks.requestBodyEndCb)
137137
ss.registerCallback(events.requestBodyProcessed(), callbacks.requestBodyObjectCb)
138138
ss.registerCallback(events.requestFilesFilenames(), callbacks.requestFilesFilenamesCb)
139+
ss.registerCallback(events.requestFilesContent(), callbacks.requestFilesContentCb)
139140
ss.registerCallback(events.responseBody(), callbacks.responseBodyObjectCb)
140141
ss.registerCallback(events.responseStarted(), callbacks.responseStartedCb)
141142
ss.registerCallback(events.responseHeader(), callbacks.responseHeaderCb)
@@ -372,6 +373,10 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
372373
false
373374
}
374375

376+
boolean testBodyFilesContent() {
377+
false
378+
}
379+
375380
boolean testBodyJson() {
376381
false
377382
}
@@ -1652,6 +1657,82 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
16521657
response.close()
16531658
}
16541659

1660+
def 'test instrumentation gateway file upload content'() {
1661+
setup:
1662+
assumeTrue(testBodyFilesContent())
1663+
RequestBody fileBody = RequestBody.create(MediaType.parse('application/octet-stream'), 'file content')
1664+
def body = new MultipartBody.Builder()
1665+
.setType(MultipartBody.FORM)
1666+
.addFormDataPart('file', 'test.bin', fileBody)
1667+
.build()
1668+
def httpRequest = request(BODY_MULTIPART, 'POST', body).build()
1669+
def response = client.newCall(httpRequest).execute()
1670+
1671+
when:
1672+
TEST_WRITER.waitForTraces(1)
1673+
1674+
then:
1675+
TEST_WRITER.get(0).any {
1676+
it.getTag('request.body.files_content') == '[file content]'
1677+
}
1678+
1679+
cleanup:
1680+
response.close()
1681+
}
1682+
1683+
def 'test instrumentation gateway file upload content truncated at max size'() {
1684+
setup:
1685+
assumeTrue(testBodyFilesContent())
1686+
def maxContentBytes = Config.get().getAppSecMaxFileContentBytes()
1687+
def body = new MultipartBody.Builder()
1688+
.setType(MultipartBody.FORM)
1689+
.addFormDataPart('file', 'large.bin',
1690+
RequestBody.create(MediaType.parse('application/octet-stream'), 'X' * (maxContentBytes + 500)))
1691+
.build()
1692+
def httpRequest = request(BODY_MULTIPART, 'POST', body).build()
1693+
def response = client.newCall(httpRequest).execute()
1694+
1695+
when:
1696+
TEST_WRITER.waitForTraces(1)
1697+
1698+
then:
1699+
TEST_WRITER.get(0).any {
1700+
span ->
1701+
span.getTag('request.body.files_content') == '[' + 'X' * maxContentBytes + ']'
1702+
}
1703+
1704+
cleanup:
1705+
response.close()
1706+
}
1707+
1708+
def 'test instrumentation gateway file upload content max files limit'() {
1709+
setup:
1710+
assumeTrue(testBodyFilesContent())
1711+
def maxFilesToInspect = Config.get().getAppSecMaxFileContentCount()
1712+
def bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM)
1713+
(1..maxFilesToInspect + 1).each {
1714+
i ->
1715+
bodyBuilder.addFormDataPart("file$i", "file${i}.bin",
1716+
RequestBody.create(MediaType.parse('application/octet-stream'), "content_of_file_$i"))
1717+
}
1718+
def httpRequest = request(BODY_MULTIPART, 'POST', bodyBuilder.build()).build()
1719+
def response = client.newCall(httpRequest).execute()
1720+
1721+
when:
1722+
TEST_WRITER.waitForTraces(1)
1723+
1724+
then:
1725+
TEST_WRITER.get(0).any {
1726+
span ->
1727+
def tag = span.getTag('request.body.files_content') as String
1728+
tag?.contains("content_of_file_$maxFilesToInspect") &&
1729+
!tag.contains("content_of_file_${maxFilesToInspect + 1}")
1730+
}
1731+
1732+
cleanup:
1733+
response.close()
1734+
}
1735+
16551736
def 'test instrumentation gateway json request body'() {
16561737
setup:
16571738
assumeTrue(testBodyJson())
@@ -2589,6 +2670,7 @@ abstract class HttpServerTest<SERVER> extends WithHttpServer<SERVER> {
25892670
boolean responseBodyTag
25902671
Object responseBody
25912672
List<String> uploadedFilenames
2673+
List<String> uploadedFilesContent
25922674
}
25932675

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

2850+
final BiFunction<RequestContext, List<String>, Flow<Void>> requestFilesContentCb =
2851+
({
2852+
RequestContext rqCtxt, List<String> contents ->
2853+
rqCtxt.traceSegment.setTagTop('request.body.files_content', contents as String)
2854+
Context context = rqCtxt.getData(RequestContextSlot.APPSEC)
2855+
context.uploadedFilesContent = contents
2856+
Flow.ResultFlow.empty()
2857+
} as BiFunction<RequestContext, List<String>, Flow<Void>>)
2858+
27682859
final BiFunction<RequestContext, Object, Flow<Void>> responseBodyObjectCb =
27692860
({
27702861
RequestContext rqCtxt, Object obj ->

dd-java-agent/instrumentation/commons-fileupload-1.5/src/main/java/datadog/trace/instrumentation/commons/fileupload/CommonsFileUploadAppSecInstrumentation.java

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
@AutoService(InstrumenterModule.class)
2727
public class CommonsFileUploadAppSecInstrumentation extends InstrumenterModule.AppSec
2828
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
29-
3029
public CommonsFileUploadAppSecInstrumentation() {
3130
super("commons-fileupload");
3231
}
@@ -54,7 +53,6 @@ public void methodAdvice(MethodTransformer transformer) {
5453

5554
@RequiresRequestContext(RequestContextSlot.APPSEC)
5655
public static class ParseRequestAdvice {
57-
5856
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
5957
static void after(
6058
@Advice.Return final List<FileItem> fileItems,
@@ -73,22 +71,22 @@ static void after(
7371
return;
7472
}
7573

76-
List<String> filenames = new ArrayList<>();
74+
List<String> filenames = filenamesCallback != null ? new ArrayList<>() : null;
75+
List<String> filesContent = contentCallback != null ? new ArrayList<>() : null;
7776
for (FileItem fileItem : fileItems) {
7877
if (fileItem.isFormField()) {
7978
continue;
8079
}
8180
String name = fileItem.getName();
82-
if (name != null && !name.isEmpty()) {
81+
if (filenames != null && name != null && !name.isEmpty()) {
8382
filenames.add(name);
8483
}
85-
}
86-
if (filenames.isEmpty() && contentCallback == null) {
87-
return;
84+
if (filesContent != null) {
85+
FileItemContentReader.addToContents(fileItem, filesContent);
86+
}
8887
}
8988

90-
// Fire filenames event
91-
if (filenamesCallback != null && !filenames.isEmpty()) {
89+
if (filenames != null && !filenames.isEmpty()) {
9290
Flow<Void> flow = filenamesCallback.apply(reqCtx, filenames);
9391
Flow.Action action = flow.getAction();
9492
if (action instanceof Flow.Action.RequestBlockingAction) {
@@ -98,28 +96,21 @@ static void after(
9896
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
9997
t = new BlockingException("Blocked request (multipart file upload)");
10098
reqCtx.getTraceSegment().effectivelyBlocked();
101-
return;
10299
}
103100
}
104101
}
105102

106-
// Fire content event only if not blocked
107-
if (contentCallback == null) {
108-
return;
109-
}
110-
List<String> filesContent = FileItemContentReader.readContents(fileItems);
111-
if (filesContent.isEmpty()) {
112-
return;
113-
}
114-
Flow<Void> contentFlow = contentCallback.apply(reqCtx, filesContent);
115-
Flow.Action contentAction = contentFlow.getAction();
116-
if (contentAction instanceof Flow.Action.RequestBlockingAction) {
117-
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) contentAction;
118-
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
119-
if (brf != null) {
120-
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
121-
t = new BlockingException("Blocked request (multipart file upload content)");
122-
reqCtx.getTraceSegment().effectivelyBlocked();
103+
if (t == null && filesContent != null && !filesContent.isEmpty()) {
104+
Flow<Void> contentFlow = contentCallback.apply(reqCtx, filesContent);
105+
Flow.Action contentAction = contentFlow.getAction();
106+
if (contentAction instanceof Flow.Action.RequestBlockingAction) {
107+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) contentAction;
108+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
109+
if (brf != null) {
110+
brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
111+
t = new BlockingException("Blocked request (multipart file upload content)");
112+
reqCtx.getTraceSegment().effectivelyBlocked();
113+
}
123114
}
124115
}
125116
}
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,31 @@
11
package datadog.trace.instrumentation.commons.fileupload;
22

3+
import datadog.trace.api.Config;
34
import datadog.trace.api.http.MultipartContentDecoder;
45
import java.io.IOException;
56
import java.io.InputStream;
6-
import java.util.ArrayList;
77
import java.util.List;
88
import org.apache.commons.fileupload.FileItem;
99

1010
/** Reads uploaded file content for WAF inspection. */
1111
public final class FileItemContentReader {
12-
public static final int MAX_CONTENT_BYTES = 4096;
13-
public static final int MAX_FILES_TO_INSPECT = 25;
14-
15-
public static List<String> readContents(List<FileItem> fileItems) {
16-
List<String> result = new ArrayList<>();
17-
for (FileItem fileItem : fileItems) {
18-
if (result.size() >= MAX_FILES_TO_INSPECT) {
19-
break;
20-
}
21-
if (fileItem.isFormField()) {
22-
continue;
23-
}
24-
result.add(readContent(fileItem));
25-
}
26-
return result;
27-
}
12+
public static final int MAX_CONTENT_BYTES = Config.get().getAppSecMaxFileContentBytes();
13+
public static final int MAX_FILES_TO_INSPECT = Config.get().getAppSecMaxFileContentCount();
2814

2915
public static String readContent(FileItem fileItem) {
3016
try (InputStream is = fileItem.getInputStream()) {
31-
byte[] buf = new byte[MAX_CONTENT_BYTES];
32-
int total = 0;
33-
int n;
34-
while (total < MAX_CONTENT_BYTES
35-
&& (n = is.read(buf, total, MAX_CONTENT_BYTES - total)) != -1) {
36-
total += n;
37-
}
38-
return MultipartContentDecoder.decodeBytes(buf, total, fileItem.getContentType());
17+
return MultipartContentDecoder.readInputStream(
18+
is, MAX_CONTENT_BYTES, fileItem.getContentType());
3919
} catch (IOException ignored) {
4020
return "";
4121
}
4222
}
4323

24+
public static void addToContents(FileItem fileItem, List<String> contents) {
25+
if (contents.size() < MAX_FILES_TO_INSPECT) {
26+
contents.add(readContent(fileItem));
27+
}
28+
}
29+
4430
private FileItemContentReader() {}
4531
}

dd-java-agent/instrumentation/commons-fileupload-1.5/src/test/groovy/FileItemContentReaderTest.groovy

Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -48,61 +48,41 @@ class FileItemContentReaderTest extends Specification {
4848
FileItemContentReader.readContent(item) == text
4949
}
5050

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

5556
when:
56-
def result = FileItemContentReader.readContents(items)
57+
FileItemContentReader.addToContents(item, contents)
5758

5859
then:
59-
result == ['content-a', 'content-b']
60+
contents == ['hello']
6061
}
6162

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

6868
when:
69-
def result = FileItemContentReader.readContents(items)
69+
FileItemContentReader.addToContents(item, contents)
7070

7171
then:
72-
result == ['content']
72+
contents.size() == FileItemContentReader.MAX_FILES_TO_INSPECT
7373
}
7474

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

8380
when:
84-
def result = FileItemContentReader.readContents(items)
81+
FileItemContentReader.addToContents(item, contents)
8582

8683
then:
87-
result == ['content-no-name', 'content-empty-name', 'content-named']
88-
}
89-
90-
void 'readContents stops after MAX_FILES_TO_INSPECT files'() {
91-
given:
92-
def items = (1..FileItemContentReader.MAX_FILES_TO_INSPECT + 1).collect {
93-
fileItem("content-${it}", "file-${it}.txt")
94-
}
95-
96-
when:
97-
def result = FileItemContentReader.readContents(items)
98-
99-
then:
100-
result.size() == FileItemContentReader.MAX_FILES_TO_INSPECT
101-
}
102-
103-
void 'readContents returns empty list for empty input'() {
104-
expect:
105-
FileItemContentReader.readContents([]) == []
84+
contents.size() == FileItemContentReader.MAX_FILES_TO_INSPECT
85+
contents.last() == 'last'
10686
}
10787

10888
private FileItem fileItem(String content) {

0 commit comments

Comments
 (0)