Skip to content
Open
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 @@ -49,6 +49,11 @@ class GrizzlyTest extends HttpServerTest<HttpServer> {
true
}

@Override
boolean testBodyFilenames() {
true
}

@Override
boolean testBodyJson() {
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ class Jersey2JettyTest extends HttpServerTest<JettyServer> {
true
}

@Override
boolean testBodyFilenames() {
true
}

@Override
boolean testBodyJson() {
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ class Jersey3JettyTest extends HttpServerTest<JettyServer> {
true
}

@Override
boolean testBodyFilenames() {
true
}

@Override
boolean testBodyJson() {
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,16 @@ muzzle {

apply from: "$rootDir/gradle/java.gradle"

configurations.configureEach {
resolutionStrategy.deactivateDependencyLocking()
}

dependencies {
compileOnly group: 'org.glassfish.jersey.core', name: 'jersey-common', version: '2.0'
compileOnly group: 'org.glassfish.jersey.core', name: 'jersey-server', version: '2.0'
compileOnly group: 'org.glassfish.jersey.media', name: 'jersey-media-multipart', version: '2.0'

testImplementation group: 'org.glassfish.jersey.media', name: 'jersey-media-multipart', version: '2.18'
}

// tested in grizzly-http-2.3.20
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package datadog.trace.instrumentation.jersey2;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.ws.rs.core.MediaType;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.message.internal.MediaTypes;

public final class MultiPartHelper {

private MultiPartHelper() {}

public static void collectBodyPart(
FormDataBodyPart bodyPart, Map<String, List<String>> bodyMap, List<String> filenames) {
if (bodyMap != null
&& MediaTypes.typeEqual(MediaType.TEXT_PLAIN_TYPE, bodyPart.getMediaType())) {
// BodyPartEntity allows re-reading the part without consuming the stream
bodyMap.computeIfAbsent(bodyPart.getName(), k -> new ArrayList<>()).add(bodyPart.getValue());
}
if (filenames != null) {
String filename = filenameFromBodyPart(bodyPart);
if (filename != null) {
filenames.add(filename);
}
}
}

public static String filenameFromBodyPart(FormDataBodyPart bodyPart) {
FormDataContentDisposition cd = bodyPart.getFormDataContentDisposition();
if (cd == null) return null;
String filename = cd.getFileName();
return (filename == null || filename.isEmpty()) ? null : filename;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import javax.ws.rs.core.MediaType;
import net.bytebuddy.asm.Advice;
import org.glassfish.jersey.media.multipart.BodyPart;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.MultiPart;
import org.glassfish.jersey.message.internal.MediaTypes;

@AutoService(InstrumenterModule.class)
public class MultiPartReaderServerSideInstrumentation extends InstrumenterModule.AppSec
Expand All @@ -48,6 +46,11 @@ public String instrumentedType() {
return "org.glassfish.jersey.media.multipart.internal.MultiPartReaderServerSide";
}

@Override
public String[] helperClassNames() {
return new String[] {packageName + ".MultiPartHelper"};
}

@Override
public void methodAdvice(MethodTransformer transformer) {
transformer.applyAdvice(
Expand All @@ -72,42 +75,47 @@ static void after(
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
BiFunction<RequestContext, Object, Flow<Void>> callback =
cbp.getCallback(EVENTS.requestBodyProcessed());
if (callback == null) {
BiFunction<RequestContext, List<String>, Flow<Void>> filenamesCallback =
cbp.getCallback(EVENTS.requestFilesFilenames());
if (callback == null && filenamesCallback == null) {
return;
}

Map<String, List<String>> map = new HashMap<>();
Map<String, List<String>> map = callback != null ? new HashMap<>() : null;
List<String> filenames = filenamesCallback != null ? new ArrayList<>() : null;
for (BodyPart bodyPart : ret.getBodyParts()) {
if (!(bodyPart instanceof FormDataBodyPart)) {
continue;
}
FormDataBodyPart dataBodyPart = (FormDataBodyPart) bodyPart;
if (!MediaTypes.typeEqual(MediaType.TEXT_PLAIN_TYPE, dataBodyPart.getMediaType())) {
continue;
}
// if the type of dataBodyPart.getEntity() is BodyPartEntity, it is safe to read the part
// more than once. So we're not depriving the application of the data by consuming it here
String v = dataBodyPart.getValue();
MultiPartHelper.collectBodyPart((FormDataBodyPart) bodyPart, map, filenames);
}

String name = dataBodyPart.getName();
List<String> values = map.get(name);
if (values == null) {
values = new ArrayList<>();
map.put(name, values);
if (map != null) {
Flow<Void> flow = callback.apply(reqCtx, map);
Flow.Action action = flow.getAction();
if (action instanceof Flow.Action.RequestBlockingAction) {
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
if (blockResponseFunction != null) {
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
t = new BlockingException("Blocked request (for MultiPartReaderClientSide/readFrom)");
reqCtx.getTraceSegment().effectivelyBlocked();
}
}

values.add(v);
}

Flow<Void> flow = callback.apply(reqCtx, map);
Flow.Action action = flow.getAction();
if (action instanceof Flow.Action.RequestBlockingAction) {
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
if (blockResponseFunction != null) {
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
t = new BlockingException("Blocked request (for MultiPartReaderClientSide/readFrom)");
reqCtx.getTraceSegment().effectivelyBlocked();
if (filenames != null && !filenames.isEmpty()) {
Flow<Void> filenamesFlow = filenamesCallback.apply(reqCtx, filenames);
Flow.Action filenamesAction = filenamesFlow.getAction();
if (t == null && filenamesAction instanceof Flow.Action.RequestBlockingAction) {
Flow.Action.RequestBlockingAction rba =
(Flow.Action.RequestBlockingAction) filenamesAction;
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
if (blockResponseFunction != null) {
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
t = new BlockingException("Blocked request (multipart file upload)");
reqCtx.getTraceSegment().effectivelyBlocked();
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import datadog.trace.instrumentation.jersey2.MultiPartHelper
import org.glassfish.jersey.media.multipart.FormDataBodyPart
import org.glassfish.jersey.media.multipart.FormDataContentDisposition
import spock.lang.Specification

import javax.ws.rs.core.MediaType

class MultiPartHelperTest extends Specification {

// filenameFromBodyPart

def "returns null when content disposition is null"() {
given:
def bodyPart = Mock(FormDataBodyPart)
bodyPart.getFormDataContentDisposition() >> null

expect:
MultiPartHelper.filenameFromBodyPart(bodyPart) == null
}

def "returns null when filename is null or empty"() {
given:
def cd = Mock(FormDataContentDisposition)
cd.getFileName() >> rawFilename
def bodyPart = Mock(FormDataBodyPart)
bodyPart.getFormDataContentDisposition() >> cd

expect:
MultiPartHelper.filenameFromBodyPart(bodyPart) == null

where:
rawFilename << [null, '']
}

def "extracts filename"() {
given:
def cd = Mock(FormDataContentDisposition)
cd.getFileName() >> filename
def bodyPart = Mock(FormDataBodyPart)
bodyPart.getFormDataContentDisposition() >> cd

expect:
MultiPartHelper.filenameFromBodyPart(bodyPart) == filename

where:
filename << ['report.php', 'upload.txt', 'shell;evil.php', 'file"name.php']
}

// collectBodyPart — body map

def "text/plain part is added to body map"() {
given:
def bodyPart = Mock(FormDataBodyPart)
bodyPart.getMediaType() >> MediaType.TEXT_PLAIN_TYPE
bodyPart.getName() >> 'field'
bodyPart.getValue() >> 'value'
bodyPart.getFormDataContentDisposition() >> null
def map = [:]

when:
MultiPartHelper.collectBodyPart(bodyPart, map, null)

then:
map == [field: ['value']]
}

def "non-text/plain part is not added to body map"() {
given:
def bodyPart = Mock(FormDataBodyPart)
bodyPart.getMediaType() >> MediaType.APPLICATION_OCTET_STREAM_TYPE
bodyPart.getFormDataContentDisposition() >> null
def map = [:]

when:
MultiPartHelper.collectBodyPart(bodyPart, map, null)

then:
map.isEmpty()
}

def "null body map is skipped without error"() {
given:
def bodyPart = Mock(FormDataBodyPart)
bodyPart.getMediaType() >> MediaType.TEXT_PLAIN_TYPE
bodyPart.getFormDataContentDisposition() >> null

expect:
MultiPartHelper.collectBodyPart(bodyPart, null, null)
}

def "multiple values for same field are accumulated"() {
given:
def bodyPart = Mock(FormDataBodyPart)
bodyPart.getMediaType() >> MediaType.TEXT_PLAIN_TYPE
bodyPart.getName() >> 'tag'
bodyPart.getValue() >>> ['a', 'b']
bodyPart.getFormDataContentDisposition() >> null
def map = [:]

when:
MultiPartHelper.collectBodyPart(bodyPart, map, null)
MultiPartHelper.collectBodyPart(bodyPart, map, null)

then:
map == [tag: ['a', 'b']]
}

// collectBodyPart — filenames

def "filename is added to list when present"() {
given:
def cd = Mock(FormDataContentDisposition)
cd.getFileName() >> 'report.php'
def bodyPart = Mock(FormDataBodyPart)
bodyPart.getMediaType() >> MediaType.APPLICATION_OCTET_STREAM_TYPE
bodyPart.getFormDataContentDisposition() >> cd
def filenames = []

when:
MultiPartHelper.collectBodyPart(bodyPart, null, filenames)

then:
filenames == ['report.php']
}

def "null filenames list is skipped without error"() {
given:
def cd = Mock(FormDataContentDisposition)
cd.getFileName() >> 'report.php'
def bodyPart = Mock(FormDataBodyPart)
bodyPart.getMediaType() >> MediaType.TEXT_PLAIN_TYPE
bodyPart.getName() >> 'f'
bodyPart.getValue() >> 'v'
bodyPart.getFormDataContentDisposition() >> cd

expect:
MultiPartHelper.collectBodyPart(bodyPart, [:], null)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,20 @@ muzzle {

apply from: "$rootDir/gradle/java.gradle"

testJvmConstraints {
minJavaVersion = JavaVersion.VERSION_11
}

configurations.configureEach {
resolutionStrategy.deactivateDependencyLocking()
}

dependencies {
compileOnly group: 'org.glassfish.jersey.core', name: 'jersey-common', version: '3.0.0'
compileOnly group: 'org.glassfish.jersey.core', name: 'jersey-server', version: '3.0.0'
compileOnly group: 'org.glassfish.jersey.media', name: 'jersey-media-multipart', version: '3.0.0'

testImplementation group: 'org.glassfish.jersey.media', name: 'jersey-media-multipart', version: '3.1.2'
}

// tested in GrizzlyTest/GrizzlyAsyncTest
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package datadog.trace.instrumentation.jersey3;

import jakarta.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.message.internal.MediaTypes;

public final class MultiPartHelper {

private MultiPartHelper() {}

public static void collectBodyPart(
FormDataBodyPart bodyPart, Map<String, List<String>> bodyMap, List<String> filenames) {
if (bodyMap != null
&& MediaTypes.typeEqual(MediaType.TEXT_PLAIN_TYPE, bodyPart.getMediaType())) {
// BodyPartEntity allows re-reading the part without consuming the stream
bodyMap.computeIfAbsent(bodyPart.getName(), k -> new ArrayList<>()).add(bodyPart.getValue());
}
if (filenames != null) {
String filename = filenameFromBodyPart(bodyPart);
if (filename != null) {
filenames.add(filename);
}
}
}

public static String filenameFromBodyPart(FormDataBodyPart bodyPart) {
FormDataContentDisposition cd = bodyPart.getFormDataContentDisposition();
if (cd == null) return null;
String filename = cd.getFileName();
return (filename == null || filename.isEmpty()) ? null : filename;
}
}
Loading
Loading