From 92bebe4ef1e3765ad24d33ccbef60e4c75f82504 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Tue, 7 Jul 2026 17:19:08 +0200 Subject: [PATCH 1/2] fix: bound unbounded multipart form-field buffering in Jetty 8 AppSec advice PartHelper.readPartContent() copied a multipart Part's InputStream into an unbounded ByteArrayOutputStream before any WAF size check applied, allowing a single oversized text field to exhaust heap. Cap the read at Config.get().getAppSecMaxFileContentBytes(), reusing the existing file-content byte cap since there is no dedicated form-field knob. --- .../trace/instrumentation/jetty8/PartHelper.java | 11 ++++++++++- .../instrumentation/jetty8/PartHelperTest.java | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) 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..143f18c894c 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; @@ -259,12 +260,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..eaa7ba28b7f 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,20 @@ 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()); + } + // ── getAllParts ───────────────────────────────────────────────────────────── @Test From 37fc40deb48ade1316646d93c5e9660afd44d97b Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Tue, 7 Jul 2026 17:39:36 +0200 Subject: [PATCH 2/2] fix: cap number of multipart form fields inspected in Jetty 8 PartHelper extractFormFields() bounded the bytes read per field but not the number of fields processed, leaving total allocation unbounded as O(fields x byteCap). Cap the field count via Config.get().getAppSecMaxFileContentCount(), mirroring the MAX_FILES_TO_INSPECT pattern used by MultipartHelper#extractContents() (PR #11706) for file content. --- .../trace/instrumentation/jetty8/PartHelper.java | 12 +++++++++++- .../trace/instrumentation/jetty8/PartHelperTest.java | 12 ++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) 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 143f18c894c..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 @@ -108,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) { @@ -130,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); } 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 eaa7ba28b7f..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 @@ -252,6 +252,18 @@ void extractFormFieldsTruncatesFieldExceedingMaxContentBytes() throws IOExceptio 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