Skip to content

Commit 92d23d9

Browse files
committed
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.
1 parent b4d0e6e commit 92d23d9

2 files changed

Lines changed: 24 additions & 12 deletions

File tree

dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/NettyFileUploadContentReader.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package datadog.trace.instrumentation.netty41;
22

33
import datadog.trace.api.Config;
4+
import datadog.trace.api.http.MultipartContentDecoder;
45
import io.netty.buffer.ByteBuf;
56
import io.netty.handler.codec.http.multipart.FileUpload;
67
import java.io.FileInputStream;
7-
import java.nio.charset.StandardCharsets;
88

99
/** Reads uploaded file content from a Netty {@link FileUpload} for WAF inspection. */
1010
public final class NettyFileUploadContentReader {
@@ -18,7 +18,7 @@ public static String readContent(FileUpload fileUpload) {
1818
int length = Math.min(MAX_CONTENT_BYTES, buf.readableBytes());
1919
byte[] bytes = new byte[length];
2020
buf.getBytes(buf.readerIndex(), bytes);
21-
return new String(bytes, StandardCharsets.ISO_8859_1);
21+
return MultipartContentDecoder.decodeBytes(bytes, length, fileUpload.getContentType());
2222
} else {
2323
byte[] bytes = new byte[MAX_CONTENT_BYTES];
2424
int total = 0;
@@ -29,7 +29,7 @@ public static String readContent(FileUpload fileUpload) {
2929
total += n;
3030
}
3131
}
32-
return new String(bytes, 0, total, StandardCharsets.ISO_8859_1);
32+
return MultipartContentDecoder.decodeBytes(bytes, total, fileUpload.getContentType());
3333
}
3434
} catch (Exception ignored) {
3535
return "";

dd-java-agent/instrumentation/tomcat/tomcat-appsec/tomcat-appsec-7.0/src/main/java/datadog/trace/instrumentation/tomcat7/ParameterCollector.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package datadog.trace.instrumentation.tomcat7;
22

33
import datadog.trace.api.Config;
4+
import datadog.trace.api.http.MultipartContentDecoder;
45
import java.io.InputStream;
56
import java.lang.reflect.Method;
6-
import java.nio.charset.StandardCharsets;
77
import java.util.AbstractMap;
88
import java.util.ArrayList;
99
import java.util.Collections;
@@ -148,27 +148,39 @@ public List<String> getContents() {
148148
// Keyed by Part concrete class; re-resolved when the class changes (different Tomcat version).
149149
private static volatile Map.Entry<Class<?>, Method> cachedInputStreamEntry;
150150
private static volatile Map.Entry<Class<?>, Method> cachedFilenameEntry;
151+
private static volatile Map.Entry<Class<?>, Method> cachedContentTypeEntry;
151152

152153
private static String readContent(Object part) {
153154
try {
154155
Class<?> partClass = part.getClass();
155-
Map.Entry<Class<?>, Method> entry = cachedInputStreamEntry;
156-
Method m;
157-
if (entry == null || entry.getKey() != partClass) {
158-
m = partClass.getMethod("getInputStream");
159-
cachedInputStreamEntry = new AbstractMap.SimpleImmutableEntry<>(partClass, m);
156+
Map.Entry<Class<?>, Method> inputStreamEntry = cachedInputStreamEntry;
157+
Method getInputStream;
158+
if (inputStreamEntry == null || inputStreamEntry.getKey() != partClass) {
159+
getInputStream = partClass.getMethod("getInputStream");
160+
cachedInputStreamEntry =
161+
new AbstractMap.SimpleImmutableEntry<>(partClass, getInputStream);
160162
} else {
161-
m = entry.getValue();
163+
getInputStream = inputStreamEntry.getValue();
162164
}
163-
try (InputStream is = (InputStream) m.invoke(part)) {
165+
Map.Entry<Class<?>, Method> contentTypeEntry = cachedContentTypeEntry;
166+
Method getContentType;
167+
if (contentTypeEntry == null || contentTypeEntry.getKey() != partClass) {
168+
getContentType = partClass.getMethod("getContentType");
169+
cachedContentTypeEntry =
170+
new AbstractMap.SimpleImmutableEntry<>(partClass, getContentType);
171+
} else {
172+
getContentType = contentTypeEntry.getValue();
173+
}
174+
String contentType = (String) getContentType.invoke(part);
175+
try (InputStream is = (InputStream) getInputStream.invoke(part)) {
164176
byte[] buf = new byte[MAX_CONTENT_BYTES];
165177
int total = 0;
166178
int n;
167179
while (total < MAX_CONTENT_BYTES
168180
&& (n = is.read(buf, total, MAX_CONTENT_BYTES - total)) != -1) {
169181
total += n;
170182
}
171-
return new String(buf, 0, total, StandardCharsets.ISO_8859_1);
183+
return MultipartContentDecoder.decodeBytes(buf, total, contentType);
172184
}
173185
} catch (Exception ignored) {
174186
return "";

0 commit comments

Comments
 (0)