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 @@ -3,6 +3,7 @@
import static datadog.trace.api.gateway.Events.EVENTS;

import datadog.appsec.api.blocking.BlockingException;
import datadog.trace.api.Config;
import datadog.trace.api.gateway.BlockResponseFunction;
import datadog.trace.api.gateway.CallbackProvider;
import datadog.trace.api.gateway.Flow;
Expand Down Expand Up @@ -107,14 +108,23 @@ public static List<String> extractFilenames(Collection<?> parts) {

/**
* Returns a name→values map of form-field parts (those without a {@code filename=} parameter).
* File-upload parts are skipped to avoid reading potentially large content.
* File-upload parts are skipped to avoid reading potentially large content. Reads up to {@link
* Config#getAppSecMaxFileContentBytes()} bytes per field, up to {@link
* Config#getAppSecMaxFileContentCount()} fields total — same knobs and cap pattern as {@code
* MultipartHelper#extractContents()} uses for file content (PR #11706), reused here since there
* is no dedicated "max form fields" config.
*/
public static Map<String, List<String>> extractFormFields(Collection<?> parts) {
if (parts == null || parts.isEmpty()) {
return Collections.emptyMap();
}
int maxFields = Config.get().getAppSecMaxFileContentCount();
Map<String, List<String>> result = new LinkedHashMap<>();
int count = 0;
for (Object obj : parts) {
if (count >= maxFields) {
break;
Comment thread
jandro996 marked this conversation as resolved.
}
try {
Part part = (Part) obj;
if (filenameFromPart(part) != null) {
Expand All @@ -129,6 +139,7 @@ public static Map<String, List<String>> extractFormFields(Collection<?> parts) {
continue;
}
result.computeIfAbsent(name, k -> new ArrayList<>()).add(value);
count++;
} catch (Exception e) {
log.debug("extractFormFields: skipping malformed part", e);
}
Expand Down Expand Up @@ -259,12 +270,20 @@ public static BlockingException fireFilenamesEvent(Collection<?> parts, RequestC

private static String readPartContent(Part part) {
Charset charset = charsetFromContentType(part.getContentType());
// Bound the buffered form-field text by the file-content byte cap. There is no dedicated
// "max form-field bytes" config, so we intentionally reuse getAppSecMaxFileContentBytes():
// this is the only framework that must manually buffer form-field text (Servlet 3.0 has no
// container-side bound), and without a cap a single huge text field could exhaust the heap.
int maxBytes = Config.get().getAppSecMaxFileContentBytes();
try (InputStream is = part.getInputStream()) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int total = 0;
int read;
while ((read = is.read(buf)) != -1) {
while (total < maxBytes
&& (read = is.read(buf, 0, Math.min(buf.length, maxBytes - total))) != -1) {
baos.write(buf, 0, read);
total += read;
}
return new String(baos.toByteArray(), charset);
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package datadog.trace.instrumentation.jetty8;

import static java.util.Arrays.asList;
import static java.util.Arrays.fill;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import datadog.trace.api.Config;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
Expand Down Expand Up @@ -236,6 +238,32 @@ void extractFormFieldsDecodesFieldUsingContentTypeCharset() throws IOException {
assertEquals(singletonList("café"), result.get("drink"));
}

@Test
void extractFormFieldsTruncatesFieldExceedingMaxContentBytes() throws IOException {
int maxBytes = Config.get().getAppSecMaxFileContentBytes();
// ASCII value larger than the cap so byte length == char length and truncation is exact.
char[] chars = new char[maxBytes * 2 + 123];
fill(chars, 'a');
String oversized = new String(chars);
List<Part> parts = singletonList(field("big", oversized));
Map<String, List<String>> result = PartHelper.extractFormFields(parts);
List<String> values = result.get("big");
assertEquals(1, values.size());
assertEquals(maxBytes, values.get(0).length());
}

@Test
void extractFormFieldsCapsAtMaxFileContentCount() throws IOException {
int maxFields = Config.get().getAppSecMaxFileContentCount();
int count = maxFields + 1;
Part[] parts = new Part[count];
for (int i = 0; i < count; i++) {
parts[i] = field("field" + i, "value" + i);
}
Map<String, List<String>> result = PartHelper.extractFormFields(asList(parts));
assertEquals(maxFields, result.size());
}

// ── getAllParts ─────────────────────────────────────────────────────────────

@Test
Expand Down