diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/PartHelper.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/PartHelper.java index 8fc2a44f22d..a7a32428213 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/PartHelper.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/main/java/datadog/trace/instrumentation/jetty8/PartHelper.java @@ -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; @@ -107,14 +108,23 @@ public static List 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> extractFormFields(Collection parts) { if (parts == null || parts.isEmpty()) { return Collections.emptyMap(); } + int maxFields = Config.get().getAppSecMaxFileContentCount(); Map> result = new LinkedHashMap<>(); + int count = 0; for (Object obj : parts) { + if (count >= maxFields) { + break; + } try { Part part = (Part) obj; if (filenameFromPart(part) != null) { @@ -129,6 +139,7 @@ public static Map> extractFormFields(Collection parts) { continue; } result.computeIfAbsent(name, k -> new ArrayList<>()).add(value); + count++; } catch (Exception e) { log.debug("extractFormFields: skipping malformed part", e); } @@ -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) { diff --git a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/test/java/datadog/trace/instrumentation/jetty8/PartHelperTest.java b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/test/java/datadog/trace/instrumentation/jetty8/PartHelperTest.java index 03c1ae51827..ef7bbc516a3 100644 --- a/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/test/java/datadog/trace/instrumentation/jetty8/PartHelperTest.java +++ b/dd-java-agent/instrumentation/jetty/jetty-appsec/jetty-appsec-8.1.3/src/test/java/datadog/trace/instrumentation/jetty8/PartHelperTest.java @@ -1,6 +1,7 @@ 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; @@ -8,6 +9,7 @@ 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; @@ -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 parts = singletonList(field("big", oversized)); + Map> result = PartHelper.extractFormFields(parts); + List 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> result = PartHelper.extractFormFields(asList(parts)); + assertEquals(maxFields, result.size()); + } + // ── getAllParts ───────────────────────────────────────────────────────────── @Test