From 9ec7fde2f4a3830455129ed8d6376bb63dae93b7 Mon Sep 17 00:00:00 2001 From: Sezen Leblay Date: Wed, 5 Feb 2025 14:13:08 +0100 Subject: [PATCH] Email HTML Injection detection in IAST (#8205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Email Injection detection in IAST --------- Co-authored-by: Alejandro González García Co-authored-by: Santiago M. Mola Co-authored-by: Mario Vidal Domínguez <60353145+Mariovido@users.noreply.github.com> --- .../java/com/datadog/iast/IastSystem.java | 4 +- .../datadog/iast/model/VulnerabilityType.java | 10 +- .../iast/sink/EmailInjectionModuleImpl.java | 20 ++++ .../iast/sink/EmailInjectionModuleTest.groovy | 47 +++++++++ .../StringEscapeUtilsCallSite.java | 2 +- .../StringEscapeUtilsCallSiteTest.groovy | 32 +++++-- .../StringEscapeUtilsCallSite.java | 2 +- .../StringEscapeUtilsCallSiteTest.groovy | 2 +- .../StringEscapeUtilsCallSite.java | 2 +- .../StringEscapeUtilsCallSiteTest.groovy | 2 +- .../instrumentation/jakarta-mail/build.gradle | 21 ++++ .../mail/JakartaMailInstrumentation.java | 69 +++++++++++++ .../mail/JakartaMailPartInstrumentation.java | 68 +++++++++++++ .../JakartaMailInstrumentationTest.groovy | 96 +++++++++++++++++++ .../src/test/java/MockTransport.java | 40 ++++++++ .../instrumentation/javax-mail/build.gradle | 20 ++++ .../javax/mail/JavaxMailInstrumentation.java | 69 +++++++++++++ .../mail/JavaxMailPartInstrumentation.java | 68 +++++++++++++ .../JavaxMailInstrumentationTest.groovy | 96 +++++++++++++++++++ .../src/test/java/MockTransport.java | 40 ++++++++ dd-smoke-tests/iast-util/build.gradle | 6 ++ .../controller/IastWebController.java | 44 +++++++++ .../controller/mock/JakartaMockTransport.java | 42 ++++++++ .../AbstractIastSpringBootTest.groovy | 65 +++++++++++++ .../spring-boot-2.6-webmvc/build.gradle | 6 ++ dd-smoke-tests/springboot/build.gradle | 5 + .../trace/api/iast/InstrumentationBridge.java | 2 + .../trace/api/iast/VulnerabilityMarks.java | 6 +- .../trace/api/iast/VulnerabilityTypes.java | 4 +- .../api/iast/sink/EmailInjectionModule.java | 8 ++ .../api/iast/VulnerabilityTypesTest.groovy | 1 + settings.gradle | 2 + 32 files changed, 885 insertions(+), 16 deletions(-) create mode 100644 dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/EmailInjectionModuleImpl.java create mode 100644 dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/EmailInjectionModuleTest.groovy create mode 100644 dd-java-agent/instrumentation/jakarta-mail/build.gradle create mode 100644 dd-java-agent/instrumentation/jakarta-mail/src/main/java/datadog/trace/instrumentation/jakarta/mail/JakartaMailInstrumentation.java create mode 100644 dd-java-agent/instrumentation/jakarta-mail/src/main/java/datadog/trace/instrumentation/jakarta/mail/JakartaMailPartInstrumentation.java create mode 100644 dd-java-agent/instrumentation/jakarta-mail/src/test/groovy/JakartaMailInstrumentationTest.groovy create mode 100644 dd-java-agent/instrumentation/jakarta-mail/src/test/java/MockTransport.java create mode 100644 dd-java-agent/instrumentation/javax-mail/build.gradle create mode 100644 dd-java-agent/instrumentation/javax-mail/src/main/java/datadog/trace/instrumentation/javax/mail/JavaxMailInstrumentation.java create mode 100644 dd-java-agent/instrumentation/javax-mail/src/main/java/datadog/trace/instrumentation/javax/mail/JavaxMailPartInstrumentation.java create mode 100644 dd-java-agent/instrumentation/javax-mail/src/test/groovy/JavaxMailInstrumentationTest.groovy create mode 100644 dd-java-agent/instrumentation/javax-mail/src/test/java/MockTransport.java create mode 100644 dd-smoke-tests/iast-util/src/main/java/datadog/smoketest/springboot/controller/mock/JakartaMockTransport.java create mode 100644 internal-api/src/main/java/datadog/trace/api/iast/sink/EmailInjectionModule.java diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java index 118be0e231b..ec63532127b 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java @@ -11,6 +11,7 @@ import com.datadog.iast.securitycontrol.IastSecurityControlTransformer; import com.datadog.iast.sink.ApplicationModuleImpl; import com.datadog.iast.sink.CommandInjectionModuleImpl; +import com.datadog.iast.sink.EmailInjectionModuleImpl; import com.datadog.iast.sink.HardcodedSecretModuleImpl; import com.datadog.iast.sink.HeaderInjectionModuleImpl; import com.datadog.iast.sink.HstsMissingHeaderModuleImpl; @@ -179,7 +180,8 @@ private static Stream iastModules( HardcodedSecretModuleImpl.class, InsecureAuthProtocolModuleImpl.class, ReflectionInjectionModuleImpl.class, - UntrustedDeserializationModuleImpl.class); + UntrustedDeserializationModuleImpl.class, + EmailInjectionModuleImpl.class); if (iast != FULLY_ENABLED) { modules = modules.filter(IastSystem::isOptOut); } diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/model/VulnerabilityType.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/model/VulnerabilityType.java index a8e8a9c9c1d..0cc7103a548 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/model/VulnerabilityType.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/model/VulnerabilityType.java @@ -2,6 +2,7 @@ import static com.datadog.iast.util.CRCUtils.update; import static datadog.trace.api.iast.VulnerabilityMarks.COMMAND_INJECTION_MARK; +import static datadog.trace.api.iast.VulnerabilityMarks.EMAIL_HTML_INJECTION_MARK; import static datadog.trace.api.iast.VulnerabilityMarks.HEADER_INJECTION_MARK; import static datadog.trace.api.iast.VulnerabilityMarks.LDAP_INJECTION_MARK; import static datadog.trace.api.iast.VulnerabilityMarks.NOT_MARKED; @@ -164,6 +165,12 @@ public interface VulnerabilityType { .excludedSources(Builder.DB_EXCLUDED) .build(); + VulnerabilityType EMAIL_HTML_INJECTION = + type(VulnerabilityTypes.EMAIL_HTML_INJECTION) + .mark(EMAIL_HTML_INJECTION_MARK) + .excludedSources(Builder.DB_EXCLUDED) + .build(); + /* All vulnerability types that have a mark. Should be updated if new vulnerabilityType with mark is added */ VulnerabilityType[] MARKED_VULNERABILITIES = { SQL_INJECTION, @@ -177,7 +184,8 @@ public interface VulnerabilityType { XSS, HEADER_INJECTION, REFLECTION_INJECTION, - UNTRUSTED_DESERIALIZATION + UNTRUSTED_DESERIALIZATION, + EMAIL_HTML_INJECTION }; String name(); diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/EmailInjectionModuleImpl.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/EmailInjectionModuleImpl.java new file mode 100644 index 00000000000..ab524c2e108 --- /dev/null +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/EmailInjectionModuleImpl.java @@ -0,0 +1,20 @@ +package com.datadog.iast.sink; + +import com.datadog.iast.Dependencies; +import com.datadog.iast.model.VulnerabilityType; +import datadog.trace.api.iast.sink.EmailInjectionModule; +import javax.annotation.Nullable; + +public class EmailInjectionModuleImpl extends SinkModuleBase implements EmailInjectionModule { + public EmailInjectionModuleImpl(final Dependencies dependencies) { + super(dependencies); + } + + @Override + public void onSendEmail(@Nullable final Object messageContent) { + if (messageContent == null) { + return; + } + checkInjection(VulnerabilityType.EMAIL_HTML_INJECTION, messageContent); + } +} diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/EmailInjectionModuleTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/EmailInjectionModuleTest.groovy new file mode 100644 index 00000000000..40bea111746 --- /dev/null +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/EmailInjectionModuleTest.groovy @@ -0,0 +1,47 @@ +package com.datadog.iast.sink + +import com.datadog.iast.IastModuleImplTestBase +import com.datadog.iast.Reporter +import com.datadog.iast.model.Vulnerability +import com.datadog.iast.model.VulnerabilityType +import com.datadog.iast.propagation.PropagationModuleImpl +import datadog.trace.api.iast.SourceTypes + +class EmailInjectionModuleTest extends IastModuleImplTestBase{ + + private EmailInjectionModuleImpl module + + def setup() { + module = new EmailInjectionModuleImpl(dependencies) + } + + @Override + protected Reporter buildReporter() { + return Mock(Reporter) + } + + def "test onSendEmail with null messageContent"() { + when: + module.onSendEmail(null) + + then: + noExceptionThrown() + } + + def "test onSendEmail with non-null messageContent"() { + given: + def messageContent = "test message" + def propagationModule = new PropagationModuleImpl() + propagationModule.taintObject(messageContent, SourceTypes.NONE) + + when: + module.onSendEmail(messageContent) + + then: + 1 * reporter.report(_, _) >> { args -> + def vulnerability = args[1] as Vulnerability + vulnerability.type == VulnerabilityType.EMAIL_HTML_INJECTION && + vulnerability.evidence == messageContent + } + } +} diff --git a/dd-java-agent/instrumentation/commons-lang-2/src/main/java/datadog/trace/instrumentation/commonslang/StringEscapeUtilsCallSite.java b/dd-java-agent/instrumentation/commons-lang-2/src/main/java/datadog/trace/instrumentation/commonslang/StringEscapeUtilsCallSite.java index 23230f2e987..b4eaa0a4ef3 100644 --- a/dd-java-agent/instrumentation/commons-lang-2/src/main/java/datadog/trace/instrumentation/commonslang/StringEscapeUtilsCallSite.java +++ b/dd-java-agent/instrumentation/commons-lang-2/src/main/java/datadog/trace/instrumentation/commonslang/StringEscapeUtilsCallSite.java @@ -25,7 +25,7 @@ public static String afterEscape( final PropagationModule module = InstrumentationBridge.PROPAGATION; if (module != null) { try { - module.taintStringIfTainted(result, input, false, VulnerabilityMarks.XSS_MARK); + module.taintStringIfTainted(result, input, false, VulnerabilityMarks.HTML_ESCAPED_MARK); } catch (final Throwable e) { module.onUnexpectedException("afterEscape threw", e); } diff --git a/dd-java-agent/instrumentation/commons-lang-2/src/test/groovy/datadog/trace/instrumentation/commonslang/StringEscapeUtilsCallSiteTest.groovy b/dd-java-agent/instrumentation/commons-lang-2/src/test/groovy/datadog/trace/instrumentation/commonslang/StringEscapeUtilsCallSiteTest.groovy index 4da4ca5d80a..9213ca96c7e 100644 --- a/dd-java-agent/instrumentation/commons-lang-2/src/test/groovy/datadog/trace/instrumentation/commonslang/StringEscapeUtilsCallSiteTest.groovy +++ b/dd-java-agent/instrumentation/commons-lang-2/src/test/groovy/datadog/trace/instrumentation/commonslang/StringEscapeUtilsCallSiteTest.groovy @@ -8,6 +8,7 @@ import groovy.transform.CompileDynamic import static datadog.trace.api.iast.VulnerabilityMarks.SQL_INJECTION_MARK import static datadog.trace.api.iast.VulnerabilityMarks.XSS_MARK +import static datadog.trace.api.iast.VulnerabilityMarks.EMAIL_HTML_INJECTION_MARK @CompileDynamic class StringEscapeUtilsCallSiteTest extends AgentTestRunner { @@ -27,15 +28,32 @@ class StringEscapeUtilsCallSiteTest extends AgentTestRunner { then: result == expected - 1 * module.taintStringIfTainted(_ as String, args[0], false, mark) + 1 * module.taintStringIfTainted(_ as String, args[0], false, XSS_MARK | EMAIL_HTML_INJECTION_MARK) 0 * _ where: - method | args | mark | expected - 'escapeHtml' | ['Ø-This is a quote'] | XSS_MARK | 'Ø-This is a quote' - 'escapeJava' | ['Ø-This is a quote'] | XSS_MARK | '\\u00D8-This is a quote' - 'escapeJavaScript' | ['Ø-This is a quote'] | XSS_MARK | '\\u00D8-This is a quote' - 'escapeXml' | ['Ø-This is a quote'] | XSS_MARK | 'Ø-This is a quote' - 'escapeSql' | ['Ø-This is a quote'] | SQL_INJECTION_MARK | 'Ø-This is a quote' + method | args | expected + 'escapeHtml' | ['Ø-This is a quote'] | 'Ø-This is a quote' + 'escapeJava' | ['Ø-This is a quote'] | '\\u00D8-This is a quote' + 'escapeJavaScript' | ['Ø-This is a quote'] | '\\u00D8-This is a quote' + 'escapeXml' | ['Ø-This is a quote'] | 'Ø-This is a quote' + } + + void 'test #method sql'() { + given: + final module = Mock(PropagationModule) + InstrumentationBridge.registerIastModule(module) + + when: + final result = TestStringEscapeUtilsSuite.&"$method".call(args) + + then: + result == expected + 1 * module.taintStringIfTainted(_ as String, args[0], false, SQL_INJECTION_MARK) + 0 * _ + + where: + method | args | expected + 'escapeSql' | ['Ø-This is a quote'] | 'Ø-This is a quote' } } diff --git a/dd-java-agent/instrumentation/commons-lang-3/src/main/java/datadog/trace/instrumentation/commonslang3/StringEscapeUtilsCallSite.java b/dd-java-agent/instrumentation/commons-lang-3/src/main/java/datadog/trace/instrumentation/commonslang3/StringEscapeUtilsCallSite.java index 4f5452e85d7..9fd76e0be7a 100644 --- a/dd-java-agent/instrumentation/commons-lang-3/src/main/java/datadog/trace/instrumentation/commonslang3/StringEscapeUtilsCallSite.java +++ b/dd-java-agent/instrumentation/commons-lang-3/src/main/java/datadog/trace/instrumentation/commonslang3/StringEscapeUtilsCallSite.java @@ -27,7 +27,7 @@ public static String afterEscape( final PropagationModule module = InstrumentationBridge.PROPAGATION; if (module != null) { try { - module.taintStringIfTainted(result, input, false, VulnerabilityMarks.XSS_MARK); + module.taintStringIfTainted(result, input, false, VulnerabilityMarks.HTML_ESCAPED_MARK); } catch (final Throwable e) { module.onUnexpectedException("afterEscape threw", e); } diff --git a/dd-java-agent/instrumentation/commons-lang-3/src/test/groovy/datadog/trace/instrumentation/commonslang3/StringEscapeUtilsCallSiteTest.groovy b/dd-java-agent/instrumentation/commons-lang-3/src/test/groovy/datadog/trace/instrumentation/commonslang3/StringEscapeUtilsCallSiteTest.groovy index f4466dbed9a..3ae8ade16e6 100644 --- a/dd-java-agent/instrumentation/commons-lang-3/src/test/groovy/datadog/trace/instrumentation/commonslang3/StringEscapeUtilsCallSiteTest.groovy +++ b/dd-java-agent/instrumentation/commons-lang-3/src/test/groovy/datadog/trace/instrumentation/commonslang3/StringEscapeUtilsCallSiteTest.groovy @@ -25,7 +25,7 @@ class StringEscapeUtilsCallSiteTest extends AgentTestRunner { then: result == expected - 1 * module.taintStringIfTainted(_ as String, args[0], false, VulnerabilityMarks.XSS_MARK) + 1 * module.taintStringIfTainted(_ as String, args[0], false, VulnerabilityMarks.HTML_ESCAPED_MARK) 0 * _ where: diff --git a/dd-java-agent/instrumentation/commons-text/src/main/java/datadog/trace/instrumentation/commonstext/StringEscapeUtilsCallSite.java b/dd-java-agent/instrumentation/commons-text/src/main/java/datadog/trace/instrumentation/commonstext/StringEscapeUtilsCallSite.java index 0541ff8dcf4..0d4cfc5ff32 100644 --- a/dd-java-agent/instrumentation/commons-text/src/main/java/datadog/trace/instrumentation/commonstext/StringEscapeUtilsCallSite.java +++ b/dd-java-agent/instrumentation/commons-text/src/main/java/datadog/trace/instrumentation/commonstext/StringEscapeUtilsCallSite.java @@ -29,7 +29,7 @@ public static String afterEscape( final PropagationModule module = InstrumentationBridge.PROPAGATION; if (module != null) { try { - module.taintStringIfTainted(result, input, false, VulnerabilityMarks.XSS_MARK); + module.taintStringIfTainted(result, input, false, VulnerabilityMarks.HTML_ESCAPED_MARK); } catch (final Throwable e) { module.onUnexpectedException("afterEscape threw", e); } diff --git a/dd-java-agent/instrumentation/commons-text/src/test/groovy/datadog/trace/instrumentation/commonstext/StringEscapeUtilsCallSiteTest.groovy b/dd-java-agent/instrumentation/commons-text/src/test/groovy/datadog/trace/instrumentation/commonstext/StringEscapeUtilsCallSiteTest.groovy index b0fdcc353ce..20c969aeb52 100644 --- a/dd-java-agent/instrumentation/commons-text/src/test/groovy/datadog/trace/instrumentation/commonstext/StringEscapeUtilsCallSiteTest.groovy +++ b/dd-java-agent/instrumentation/commons-text/src/test/groovy/datadog/trace/instrumentation/commonstext/StringEscapeUtilsCallSiteTest.groovy @@ -25,7 +25,7 @@ class StringEscapeUtilsCallSiteTest extends AgentTestRunner { then: result == expected - 1 * module.taintStringIfTainted(_ as String, args[0], false, VulnerabilityMarks.XSS_MARK) + 1 * module.taintStringIfTainted(_ as String, args[0], false, VulnerabilityMarks.HTML_ESCAPED_MARK) 0 * _ where: diff --git a/dd-java-agent/instrumentation/jakarta-mail/build.gradle b/dd-java-agent/instrumentation/jakarta-mail/build.gradle new file mode 100644 index 00000000000..2e00ea11b75 --- /dev/null +++ b/dd-java-agent/instrumentation/jakarta-mail/build.gradle @@ -0,0 +1,21 @@ +muzzle { + pass { + group = 'jakarta.mail' + module = 'jakarta.mail-api' + versions = '[2.0.1, ]' + } +} + +apply from: "$rootDir/gradle/java.gradle" + +addTestSuiteForDir('latestDepTest', 'test') + +dependencies { + testRuntimeOnly project(':dd-java-agent:instrumentation:iast-instrumenter') + compileOnly 'jakarta.mail:jakarta.mail-api:2.0.1' + testImplementation 'jakarta.mail:jakarta.mail-api:2.0.1' + compileOnly 'com.sun.mail:jakarta.mail:2.0.1' + testImplementation 'com.sun.mail:jakarta.mail:2.0.1' + compileOnly 'jakarta.activation:jakarta.activation-api:2.0.1' + testImplementation 'jakarta.activation:jakarta.activation-api:2.0.1' +} diff --git a/dd-java-agent/instrumentation/jakarta-mail/src/main/java/datadog/trace/instrumentation/jakarta/mail/JakartaMailInstrumentation.java b/dd-java-agent/instrumentation/jakarta-mail/src/main/java/datadog/trace/instrumentation/jakarta/mail/JakartaMailInstrumentation.java new file mode 100644 index 00000000000..3cb3c857909 --- /dev/null +++ b/dd-java-agent/instrumentation/jakarta-mail/src/main/java/datadog/trace/instrumentation/jakarta/mail/JakartaMailInstrumentation.java @@ -0,0 +1,69 @@ +package datadog.trace.instrumentation.jakarta.mail; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.iast.InstrumentationBridge; +import datadog.trace.api.iast.Sink; +import datadog.trace.api.iast.VulnerabilityTypes; +import datadog.trace.api.iast.sink.EmailInjectionModule; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Multipart; +import jakarta.mail.Part; +import java.io.IOException; +import net.bytebuddy.asm.Advice; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@AutoService(InstrumenterModule.class) +public class JakartaMailInstrumentation extends InstrumenterModule.Iast + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + + private static Logger LOGGER = LoggerFactory.getLogger(JakartaMailInstrumentation.class); + + public JakartaMailInstrumentation() { + super("jakarta-mail", "jakarta-mail-transport"); + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("send0").and(takesArgument(0, named("jakarta.mail.Message"))), + JakartaMailInstrumentation.class.getName() + "$MailInjectionAdvice"); + } + + @Override + public String instrumentedType() { + return "jakarta.mail.Transport"; + } + + public static class MailInjectionAdvice { + @Sink(VulnerabilityTypes.EMAIL_HTML_INJECTION) + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSend(@Advice.Argument(0) final Message message) + throws MessagingException, IOException { + EmailInjectionModule emailInjectionModule = InstrumentationBridge.EMAIL_INJECTION; + if (emailInjectionModule == null) { + return; + } + if (message == null || message.getContent() == null) { + return; + } + if (message.isMimeType("text/html")) { + emailInjectionModule.onSendEmail(message.getContent()); + } else if (message.isMimeType("multipart/*")) { + Multipart parts = (Multipart) message.getContent(); + for (int i = 0; i < parts.getCount(); i++) { + final Part part = parts.getBodyPart(i); + if (part != null && part.isMimeType("text/html")) { + emailInjectionModule.onSendEmail(part.getContent()); + } + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/jakarta-mail/src/main/java/datadog/trace/instrumentation/jakarta/mail/JakartaMailPartInstrumentation.java b/dd-java-agent/instrumentation/jakarta-mail/src/main/java/datadog/trace/instrumentation/jakarta/mail/JakartaMailPartInstrumentation.java new file mode 100644 index 00000000000..cfdb4ffd8be --- /dev/null +++ b/dd-java-agent/instrumentation/jakarta-mail/src/main/java/datadog/trace/instrumentation/jakarta/mail/JakartaMailPartInstrumentation.java @@ -0,0 +1,68 @@ +package datadog.trace.instrumentation.jakarta.mail; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.iast.InstrumentationBridge; +import datadog.trace.api.iast.Propagation; +import datadog.trace.api.iast.propagation.PropagationModule; +import jakarta.mail.Part; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumenterModule.class) +public class JakartaMailPartInstrumentation extends InstrumenterModule.Iast + implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice { + + public JakartaMailPartInstrumentation() { + super("jakarta-mail", "jakarta-mail-body"); + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("setContent").and(takesArgument(0, Object.class)), + JakartaMailPartInstrumentation.class.getName() + "$ContentInjectionAdvice"); + transformer.applyAdvice( + named("setText").and(takesArgument(0, String.class)), + JakartaMailPartInstrumentation.class.getName() + "$TextInjectionAdvice"); + } + + @Override + public String hierarchyMarkerType() { + return "jakarta.mail.Part"; + } + + @Override + public ElementMatcher hierarchyMatcher() { + return implementsInterface(named(hierarchyMarkerType())); + } + + public static class ContentInjectionAdvice { + @Propagation + @Advice.OnMethodEnter(suppress = Throwable.class) + private static void onSetContent( + @Advice.This Part part, @Advice.Argument(0) final Object content) { + PropagationModule propagationModule = InstrumentationBridge.PROPAGATION; + if (propagationModule != null && content != null) { + propagationModule.taintObjectIfTainted(part, content); + } + } + } + + public static class TextInjectionAdvice { + @Propagation + @Advice.OnMethodEnter(suppress = Throwable.class) + private static void onSetText(@Advice.This Part part, @Advice.Argument(0) final String text) { + PropagationModule propagationModule = InstrumentationBridge.PROPAGATION; + if (propagationModule != null && text != null) { + propagationModule.taintObjectIfTainted(part, text); + } + } + } +} diff --git a/dd-java-agent/instrumentation/jakarta-mail/src/test/groovy/JakartaMailInstrumentationTest.groovy b/dd-java-agent/instrumentation/jakarta-mail/src/test/groovy/JakartaMailInstrumentationTest.groovy new file mode 100644 index 00000000000..10e199d447b --- /dev/null +++ b/dd-java-agent/instrumentation/jakarta-mail/src/test/groovy/JakartaMailInstrumentationTest.groovy @@ -0,0 +1,96 @@ +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.iast.InstrumentationBridge +import datadog.trace.api.iast.sink.EmailInjectionModule +import jakarta.mail.Provider +import jakarta.mail.internet.MimeMessage +import jakarta.mail.internet.InternetAddress +import jakarta.mail.Message +import jakarta.mail.Session +import jakarta.mail.Transport +import jakarta.mail.internet.MimeBodyPart +import jakarta.mail.internet.MimeMultipart + + +class JakartaMailInstrumentationTest extends AgentTestRunner { + + @Override + void configurePreAgent() { + injectSysConfig("dd.iast.enabled", "true") + } + + + void 'test jakarta mail Message HTML text'() { + given: + final module = Mock(EmailInjectionModule) + InstrumentationBridge.registerIastModule(module) + def session = Session.getDefaultInstance(new Properties()) + def provider = new Provider(Provider.Type.TRANSPORT, "smtp", MockTransport.name, "MockTransport", "1.0") + session.setProvider(provider) + final message = new MimeMessage(session) + message.setRecipient(Message.RecipientType.TO, new InternetAddress("mock@datadoghq.com")) + message.setText(content, "utf-8", mimetype) + + when: + Transport.send(message) + + then: + 1 * module.onSendEmail(message.getContent()) + + where: + mimetype | content + "html" | "Hello, Content!" + } + + void 'test jakarta mail Message Plain text'() { + given: + final module = Mock(EmailInjectionModule) + InstrumentationBridge.registerIastModule(module) + def session = Session.getDefaultInstance(new Properties()) + def provider = new Provider(Provider.Type.TRANSPORT, "smtp", MockTransport.name, "MockTransport", "1.0") + session.setProvider(provider) + final message = new MimeMessage(session) + message.setRecipient(Message.RecipientType.TO, new InternetAddress("mock@datadoghq.com")) + message.setText(content, "utf-8", mimetype) + + when: + Transport.send(message) + + then: + 0 * module.onSendEmail(message.getContent()) + + where: + mimetype | content + "plain" | "Hello, Content!" + } + + void 'test jakarta mail Message Content'() { + given: + final module = Mock(EmailInjectionModule) + InstrumentationBridge.registerIastModule(module) + def session = Session.getDefaultInstance(new Properties()) + def provider = new Provider(Provider.Type.TRANSPORT, "smtp", MockTransport.name, "MockTransport", "1.0") + session.setProvider(provider) + final message = new MimeMessage(session) + message.setRecipient(Message.RecipientType.TO, new InternetAddress("mock@datadoghq.com")) + + MimeMultipart content = new MimeMultipart() + content.addBodyPart(new MimeBodyPart()) + content.addBodyPart(new MimeBodyPart()) + content.getBodyPart(0).setContent(body[0], "text/plain") + content.getBodyPart(1).setContent(body[1], "text/html") + message.setContent(content, mimetype) + + when: + Transport.send(message) + + then: + 0 * module.onSendEmail(((MimeMultipart)message.getContent()).getBodyPart(0).getContent()) + 1 * module.onSendEmail(((MimeMultipart)message.getContent()).getBodyPart(1).getContent()) + + where: + mimetype | body + "multipart/*" | new String[]{ + "Hello, Content!", "Evil Content!" + } + } +} diff --git a/dd-java-agent/instrumentation/jakarta-mail/src/test/java/MockTransport.java b/dd-java-agent/instrumentation/jakarta-mail/src/test/java/MockTransport.java new file mode 100644 index 00000000000..9b0c9eb7b8e --- /dev/null +++ b/dd-java-agent/instrumentation/jakarta-mail/src/test/java/MockTransport.java @@ -0,0 +1,40 @@ +import jakarta.mail.Address; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.URLName; +import java.util.Properties; + +public class MockTransport extends Transport { + public MockTransport(Session session, URLName urlname) { + super(session, urlname); + } + + public MockTransport() { + this(Session.getInstance(new Properties()), null); + } + + public MockTransport(Session session) { + this(session, null); + } + + public static Transport newInstance(Session session) { + return new MockTransport(session, null); + } + + public void sendMessage(Message msg, Address[] addresses) throws MessagingException { + for (Address a : addresses) { + this.notifyTransportListeners(1, addresses, new Address[0], new Address[0], msg); + } + } + + @Override + public void connect() { + this.setConnected(true); + this.notifyConnectionListeners(1); + } + + public synchronized void connect(String host, int port, String user, String password) + throws MessagingException {} +} diff --git a/dd-java-agent/instrumentation/javax-mail/build.gradle b/dd-java-agent/instrumentation/javax-mail/build.gradle new file mode 100644 index 00000000000..33e49b42ba6 --- /dev/null +++ b/dd-java-agent/instrumentation/javax-mail/build.gradle @@ -0,0 +1,20 @@ +muzzle { + pass { + group = 'javax.mail' + module = 'javax.mail-api' + versions = '[1.4.4, ]' + } +} + +apply from: "$rootDir/gradle/java.gradle" + +addTestSuiteForDir('latestDepTest', 'test') + +dependencies { + testRuntimeOnly project(':dd-java-agent:instrumentation:iast-instrumenter') + compileOnly 'javax.mail:javax.mail-api:1.4.4' + compileOnly 'com.sun.mail:javax.mail:1.4.4' + testImplementation 'com.sun.mail:javax.mail:1.4.4' + compileOnly 'javax.activation:activation:1.1.1' + testImplementation 'javax.activation:activation:1.1.1' +} diff --git a/dd-java-agent/instrumentation/javax-mail/src/main/java/datadog/trace/instrumentation/javax/mail/JavaxMailInstrumentation.java b/dd-java-agent/instrumentation/javax-mail/src/main/java/datadog/trace/instrumentation/javax/mail/JavaxMailInstrumentation.java new file mode 100644 index 00000000000..89df51adfd5 --- /dev/null +++ b/dd-java-agent/instrumentation/javax-mail/src/main/java/datadog/trace/instrumentation/javax/mail/JavaxMailInstrumentation.java @@ -0,0 +1,69 @@ +package datadog.trace.instrumentation.javax.mail; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.iast.InstrumentationBridge; +import datadog.trace.api.iast.Sink; +import datadog.trace.api.iast.VulnerabilityTypes; +import datadog.trace.api.iast.sink.EmailInjectionModule; +import java.io.IOException; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Part; +import net.bytebuddy.asm.Advice; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@AutoService(InstrumenterModule.class) +public class JavaxMailInstrumentation extends InstrumenterModule.Iast + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + + private static Logger LOGGER = LoggerFactory.getLogger(JavaxMailInstrumentation.class); + + public JavaxMailInstrumentation() { + super("javax-mail", "javax-mail-transport"); + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("send0").and(takesArgument(0, named("javax.mail.Message"))), + JavaxMailInstrumentation.class.getName() + "$MailInjectionAdvice"); + } + + @Override + public String instrumentedType() { + return "javax.mail.Transport"; + } + + public static class MailInjectionAdvice { + @Sink(VulnerabilityTypes.EMAIL_HTML_INJECTION) + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSend(@Advice.Argument(0) final Message message) + throws MessagingException, IOException { + EmailInjectionModule emailInjectionModule = InstrumentationBridge.EMAIL_INJECTION; + if (emailInjectionModule == null) { + return; + } + if (message == null || message.getContent() == null) { + return; + } + if (message.isMimeType("text/html")) { + emailInjectionModule.onSendEmail(message.getContent()); + } else if (message.isMimeType("multipart/*")) { + Multipart parts = (Multipart) message.getContent(); + for (int i = 0; i < parts.getCount(); i++) { + final Part part = parts.getBodyPart(i); + if (part != null && part.isMimeType("text/html")) { + emailInjectionModule.onSendEmail(part.getContent()); + } + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/javax-mail/src/main/java/datadog/trace/instrumentation/javax/mail/JavaxMailPartInstrumentation.java b/dd-java-agent/instrumentation/javax-mail/src/main/java/datadog/trace/instrumentation/javax/mail/JavaxMailPartInstrumentation.java new file mode 100644 index 00000000000..1e1126541df --- /dev/null +++ b/dd-java-agent/instrumentation/javax-mail/src/main/java/datadog/trace/instrumentation/javax/mail/JavaxMailPartInstrumentation.java @@ -0,0 +1,68 @@ +package datadog.trace.instrumentation.javax.mail; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.iast.InstrumentationBridge; +import datadog.trace.api.iast.Propagation; +import datadog.trace.api.iast.propagation.PropagationModule; +import javax.mail.Part; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumenterModule.class) +public class JavaxMailPartInstrumentation extends InstrumenterModule.Iast + implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice { + + public JavaxMailPartInstrumentation() { + super("javax-mail", "javax-mail-body"); + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("setContent").and(takesArgument(0, Object.class)), + JavaxMailPartInstrumentation.class.getName() + "$ContentInjectionAdvice"); + transformer.applyAdvice( + named("setText").and(takesArgument(0, String.class)), + JavaxMailPartInstrumentation.class.getName() + "$TextInjectionAdvice"); + } + + @Override + public String hierarchyMarkerType() { + return "javax.mail.Part"; + } + + @Override + public ElementMatcher hierarchyMatcher() { + return implementsInterface(named(hierarchyMarkerType())); + } + + public static class ContentInjectionAdvice { + @Propagation + @Advice.OnMethodEnter(suppress = Throwable.class) + private static void onSetContent( + @Advice.This Part part, @Advice.Argument(0) final Object content) { + PropagationModule propagationModule = InstrumentationBridge.PROPAGATION; + if (propagationModule != null && content != null) { + propagationModule.taintObjectIfTainted(part, content); + } + } + } + + public static class TextInjectionAdvice { + @Propagation + @Advice.OnMethodEnter(suppress = Throwable.class) + private static void onSetText(@Advice.This Part part, @Advice.Argument(0) final String text) { + PropagationModule propagationModule = InstrumentationBridge.PROPAGATION; + if (propagationModule != null && text != null) { + propagationModule.taintObjectIfTainted(part, text); + } + } + } +} diff --git a/dd-java-agent/instrumentation/javax-mail/src/test/groovy/JavaxMailInstrumentationTest.groovy b/dd-java-agent/instrumentation/javax-mail/src/test/groovy/JavaxMailInstrumentationTest.groovy new file mode 100644 index 00000000000..b349294be43 --- /dev/null +++ b/dd-java-agent/instrumentation/javax-mail/src/test/groovy/JavaxMailInstrumentationTest.groovy @@ -0,0 +1,96 @@ +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.iast.InstrumentationBridge +import datadog.trace.api.iast.sink.EmailInjectionModule + +import javax.mail.Transport +import javax.mail.Message +import javax.mail.Session +import javax.mail.internet.InternetAddress +import javax.mail.internet.MimeBodyPart +import javax.mail.internet.MimeMessage +import javax.mail.Provider + +import javax.mail.internet.MimeMultipart + + +class JavaxMailInstrumentationTest extends AgentTestRunner { + @Override + void configurePreAgent() { + injectSysConfig("dd.iast.enabled", "true") + } + + void 'test javax mail Message HTML text'() { + given: + final module = Mock(EmailInjectionModule) + InstrumentationBridge.registerIastModule(module) + def session = Session.getDefaultInstance(new Properties()) + def provider = new Provider(Provider.Type.TRANSPORT, "smtp", MockTransport.name, "MockTransport", "1.0") + session.setProvider(provider) + final message = new MimeMessage(session) + message.setRecipient(Message.RecipientType.TO, new InternetAddress("mock@datadoghq.com")) + message.setText(content, "utf-8", mimetype) + + when: + Transport.send(message) + + then: + 1 * module.onSendEmail(message.getContent()) + + where: + mimetype | content + "html" | "Hello, Content!" + } + + void 'test javax mail Message Plain text'() { + given: + final module = Mock(EmailInjectionModule) + InstrumentationBridge.registerIastModule(module) + def session = Session.getDefaultInstance(new Properties()) + def provider = new Provider(Provider.Type.TRANSPORT, "smtp", MockTransport.name, "MockTransport", "1.0") + session.setProvider(provider) + final message = new MimeMessage(session) + message.setRecipient(Message.RecipientType.TO, new InternetAddress("mock@datadoghq.com")) + message.setText(content, "utf-8", mimetype) + + when: + Transport.send(message) + + then: + 0 * module.onSendEmail(message.getContent()) + + where: + mimetype | content + "plain" | "Hello, Content!" + } + + void 'test javax mail Message Content'() { + given: + final module = Mock(EmailInjectionModule) + InstrumentationBridge.registerIastModule(module) + def session = Session.getDefaultInstance(new Properties()) + def provider = new Provider(Provider.Type.TRANSPORT, "smtp", MockTransport.name, "MockTransport", "1.0") + session.setProvider(provider) + final message = new MimeMessage(session) + message.setRecipient(Message.RecipientType.TO, new InternetAddress("mock@datadoghq.com")) + + MimeMultipart content = new MimeMultipart() + content.addBodyPart(new MimeBodyPart()) + content.addBodyPart(new MimeBodyPart()) + content.getBodyPart(0).setContent(body[0], "text/plain") + content.getBodyPart(1).setContent(body[1], "text/html") + message.setContent(content, mimetype) + + when: + Transport.send(message) + + then: + 0 * module.onSendEmail(((MimeMultipart)message.getContent()).getBodyPart(0).getContent()) + 1 * module.onSendEmail(((MimeMultipart)message.getContent()).getBodyPart(1).getContent()) + + where: + mimetype | body + "multipart/*" | new String[]{ + "Hello, Content!", "Evil Content!" + } + } +} diff --git a/dd-java-agent/instrumentation/javax-mail/src/test/java/MockTransport.java b/dd-java-agent/instrumentation/javax-mail/src/test/java/MockTransport.java new file mode 100644 index 00000000000..ad7fd030fff --- /dev/null +++ b/dd-java-agent/instrumentation/javax-mail/src/test/java/MockTransport.java @@ -0,0 +1,40 @@ +import java.util.Properties; +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.URLName; + +public class MockTransport extends Transport { + public MockTransport(Session session, URLName urlname) { + super(session, urlname); + } + + public MockTransport() { + this(Session.getInstance(new Properties()), null); + } + + public MockTransport(Session session) { + this(session, null); + } + + public static Transport newInstance(Session session) { + return new MockTransport(session, null); + } + + public void sendMessage(Message msg, Address[] addresses) throws MessagingException { + for (Address a : addresses) { + this.notifyTransportListeners(1, addresses, new Address[0], new Address[0], msg); + } + } + + @Override + public void connect() { + this.setConnected(true); + this.notifyConnectionListeners(1); + } + + public synchronized void connect(String host, int port, String user, String password) + throws MessagingException {} +} diff --git a/dd-smoke-tests/iast-util/build.gradle b/dd-smoke-tests/iast-util/build.gradle index a7d97b6c75f..ab33dca3efd 100644 --- a/dd-smoke-tests/iast-util/build.gradle +++ b/dd-smoke-tests/iast-util/build.gradle @@ -19,4 +19,10 @@ dependencies { compileOnly group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.0.0' compileOnly group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0' compileOnly group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.0' + // mail + implementation 'jakarta.mail:jakarta.mail-api:2.0.1' + implementation 'jakarta.activation:jakarta.activation-api:2.1.3' + implementation 'com.sun.mail:jakarta.mail:2.0.1' + // text sanitization + implementation group: 'org.apache.commons', name: 'commons-text', version: '1.0' } diff --git a/dd-smoke-tests/iast-util/src/main/java/datadog/smoketest/springboot/controller/IastWebController.java b/dd-smoke-tests/iast-util/src/main/java/datadog/smoketest/springboot/controller/IastWebController.java index 3cfd1ca77f0..6545f3f3625 100644 --- a/dd-smoke-tests/iast-util/src/main/java/datadog/smoketest/springboot/controller/IastWebController.java +++ b/dd-smoke-tests/iast-util/src/main/java/datadog/smoketest/springboot/controller/IastWebController.java @@ -4,6 +4,7 @@ import com.google.gson.JsonParser; import datadog.communication.util.IOUtils; import datadog.smoketest.springboot.TestBean; +import datadog.smoketest.springboot.controller.mock.JakartaMockTransport; import ddtest.client.sources.Hasher; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.File; @@ -18,6 +19,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; import javax.servlet.ServletException; @@ -33,6 +35,8 @@ import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.text.StringEscapeUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; @@ -332,6 +336,46 @@ public String handleFileUpload( return "fileName: " + file.getName(); } + @PostMapping("/jakartaMailHtmlVulnerability") + public String jakartaMailHtmlVulnerability(HttpServletRequest request) + throws jakarta.mail.MessagingException { + jakarta.mail.Session session = jakarta.mail.Session.getDefaultInstance(new Properties()); + jakarta.mail.Provider provider = + new jakarta.mail.Provider( + jakarta.mail.Provider.Type.TRANSPORT, + "smtp", + JakartaMockTransport.class.getName(), + "MockTransport", + "1.0"); + session.setProvider(provider); + boolean sanitize = + StringUtils.isNotEmpty(request.getParameter("sanitize")) + && request.getParameter("sanitize").equalsIgnoreCase("true"); + jakarta.mail.internet.MimeMessage message = new jakarta.mail.internet.MimeMessage(session); + if (request.getParameter("messageText") != null) { + message.setText( + sanitize + ? StringEscapeUtils.escapeHtml4(request.getParameter("messageText")) + : request.getParameter("messageText"), + "utf-8", + "html"); + } else { + jakarta.mail.Multipart content = new jakarta.mail.internet.MimeMultipart(); + content.addBodyPart(new jakarta.mail.internet.MimeBodyPart()); + content + .getBodyPart(0) + .setContent( + sanitize + ? StringEscapeUtils.escapeHtml4(request.getParameter("messageContent")) + : request.getParameter("messageContent"), + "text/html"); + message.setContent(content, "multipart/*"); + } + message.setRecipients(jakarta.mail.Message.RecipientType.TO, "abc@datadoghq.com"); + jakarta.mail.Transport.send(message); + return "ok"; + } + @GetMapping(value = "/xcontenttypeoptionsecure", produces = "text/html") public String xContentTypeOptionsSecure(HttpServletResponse response) { response.addHeader("X-Content-Type-Options", "nosniff"); diff --git a/dd-smoke-tests/iast-util/src/main/java/datadog/smoketest/springboot/controller/mock/JakartaMockTransport.java b/dd-smoke-tests/iast-util/src/main/java/datadog/smoketest/springboot/controller/mock/JakartaMockTransport.java new file mode 100644 index 00000000000..405ae95199b --- /dev/null +++ b/dd-smoke-tests/iast-util/src/main/java/datadog/smoketest/springboot/controller/mock/JakartaMockTransport.java @@ -0,0 +1,42 @@ +package datadog.smoketest.springboot.controller.mock; + +import jakarta.mail.Address; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.URLName; +import java.util.Properties; + +public class JakartaMockTransport extends Transport { + public JakartaMockTransport(Session session, URLName urlname) { + super(session, urlname); + } + + public JakartaMockTransport() { + this(Session.getInstance(new Properties()), null); + } + + public JakartaMockTransport(Session session) { + this(session, null); + } + + public static Transport newInstance(Session session) { + return new JakartaMockTransport(session, null); + } + + public void sendMessage(Message msg, Address[] addresses) throws MessagingException { + for (Address a : addresses) { + this.notifyTransportListeners(1, addresses, new Address[0], new Address[0], msg); + } + } + + @Override + public void connect() { + this.setConnected(true); + this.notifyConnectionListeners(1); + } + + public synchronized void connect(String host, int port, String user, String password) + throws MessagingException {} +} diff --git a/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractIastSpringBootTest.groovy b/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractIastSpringBootTest.groovy index 916dedf17cf..f01b4305a62 100644 --- a/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractIastSpringBootTest.groovy +++ b/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractIastSpringBootTest.groovy @@ -102,6 +102,71 @@ abstract class AbstractIastSpringBootTest extends AbstractIastServerSmokeTest { } + void 'Tainted mail Text Jakarta'() { + given: + String url = "http://localhost:${httpPort}/jakartaMailHtmlVulnerability" + String messageText = "This is a test message" + RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM) + .addFormDataPart("messageText", messageText) + .addFormDataPart("sanitize", "false") + .build() + Request request = new Request.Builder() + .url(url) + .post(requestBody) + .build() + + when: + client.newCall(request).execute().body().string() + + then: + hasVulnerability { vulnerability -> + vulnerability.type == 'EMAIL_HTML_INJECTION' + } + } + + + void 'Tainted mail Content Jakarta'() { + given: + String url = "http://localhost:${httpPort}/jakartaMailHtmlVulnerability" + String messageContent = "

This is a test message

" + RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM) + .addFormDataPart("messageContent", messageContent).build() + Request request = new Request.Builder() + .url(url) + .post(requestBody) + .build() + + when: + client.newCall(request).execute().body().string() + + then: + hasVulnerability { vulnerability -> + vulnerability.type == 'EMAIL_HTML_INJECTION' + } + } + + void 'Sanitized mail Content Jakarta'() { + given: + String url = "http://localhost:${httpPort}/jakartaMailHtmlVulnerability" + String messageContent = "

This is a test message

" + RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM) + .addFormDataPart("messageContent", messageContent) + .addFormDataPart("sanitize", "true") + .build() + Request request = new Request.Builder() + .url(url) + .post(requestBody) + .build() + + when: + client.newCall(request).execute().body().string() + + then: + noVulnerability { vulnerability -> + vulnerability.type == 'EMAIL_HTML_INJECTION' + } + } + void 'Multipart Request original file name'() { given: String url = "http://localhost:${httpPort}/multipart" diff --git a/dd-smoke-tests/spring-boot-2.6-webmvc/build.gradle b/dd-smoke-tests/spring-boot-2.6-webmvc/build.gradle index e0003f7668b..a9970816ed6 100644 --- a/dd-smoke-tests/spring-boot-2.6-webmvc/build.gradle +++ b/dd-smoke-tests/spring-boot-2.6-webmvc/build.gradle @@ -48,6 +48,12 @@ dependencies { testImplementation project(':dd-smoke-tests') implementation project(':dd-smoke-tests:iast-util') testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) + + implementation 'jakarta.mail:jakarta.mail-api:2.0.1' + implementation 'com.sun.mail:jakarta.mail:2.0.1' + implementation 'jakarta.activation:jakarta.activation-api:2.1.3' + // text sanitization + implementation group: 'org.apache.commons', name: 'commons-text', version: '1.0' } tasks.withType(Test).configureEach { diff --git a/dd-smoke-tests/springboot/build.gradle b/dd-smoke-tests/springboot/build.gradle index fc4edc573c5..3e0161aa2cb 100644 --- a/dd-smoke-tests/springboot/build.gradle +++ b/dd-smoke-tests/springboot/build.gradle @@ -40,6 +40,11 @@ dependencies { testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) implementation project(':dd-smoke-tests:iast-util') + implementation 'jakarta.mail:jakarta.mail-api:2.0.1' + implementation 'jakarta.activation:jakarta.activation-api:2.1.3' + implementation 'com.sun.mail:jakarta.mail:2.0.1' + // text sanitization + implementation group: 'org.apache.commons', name: 'commons-text', version: '1.0' } tasks.withType(Test).configureEach { diff --git a/internal-api/src/main/java/datadog/trace/api/iast/InstrumentationBridge.java b/internal-api/src/main/java/datadog/trace/api/iast/InstrumentationBridge.java index adbba693ff9..7c5c9d1ae3f 100644 --- a/internal-api/src/main/java/datadog/trace/api/iast/InstrumentationBridge.java +++ b/internal-api/src/main/java/datadog/trace/api/iast/InstrumentationBridge.java @@ -5,6 +5,7 @@ import datadog.trace.api.iast.propagation.StringModule; import datadog.trace.api.iast.sink.ApplicationModule; import datadog.trace.api.iast.sink.CommandInjectionModule; +import datadog.trace.api.iast.sink.EmailInjectionModule; import datadog.trace.api.iast.sink.HardcodedSecretModule; import datadog.trace.api.iast.sink.HeaderInjectionModule; import datadog.trace.api.iast.sink.HstsMissingHeaderModule; @@ -67,6 +68,7 @@ public abstract class InstrumentationBridge { public static InsecureAuthProtocolModule INSECURE_AUTH_PROTOCOL; public static ReflectionInjectionModule REFLECTION_INJECTION; public static UntrustedDeserializationModule UNTRUSTED_DESERIALIZATION; + public static EmailInjectionModule EMAIL_INJECTION; private static final Map, Field> MODULE_MAP = buildModuleMap(); diff --git a/internal-api/src/main/java/datadog/trace/api/iast/VulnerabilityMarks.java b/internal-api/src/main/java/datadog/trace/api/iast/VulnerabilityMarks.java index adcc65222ed..ccad045ab18 100644 --- a/internal-api/src/main/java/datadog/trace/api/iast/VulnerabilityMarks.java +++ b/internal-api/src/main/java/datadog/trace/api/iast/VulnerabilityMarks.java @@ -22,9 +22,11 @@ private VulnerabilityMarks() {} public static final int UNTRUSTED_DESERIALIZATION_MARK = 1 << 11; public static final int CUSTOM_SECURITY_CONTROL_MARK = 1 << 13; + public static final int EMAIL_HTML_INJECTION_MARK = 1 << 14; + public static final int HTML_ESCAPED_MARK = XSS_MARK | EMAIL_HTML_INJECTION_MARK; public static int markForAll() { - return XSS_MARK + return HTML_ESCAPED_MARK | XPATH_INJECTION_MARK | SQL_INJECTION_MARK | COMMAND_INJECTION_MARK @@ -67,6 +69,8 @@ public static int getMarkFromVulnerabitityType(final String vulnerabilityTypeStr return UNTRUSTED_DESERIALIZATION_MARK; case "CUSTOM_SECURITY_CONTROL": return CUSTOM_SECURITY_CONTROL_MARK; + case "EMAIL_HTML_INJECTION": + return EMAIL_HTML_INJECTION_MARK; default: return NOT_MARKED; } diff --git a/internal-api/src/main/java/datadog/trace/api/iast/VulnerabilityTypes.java b/internal-api/src/main/java/datadog/trace/api/iast/VulnerabilityTypes.java index 4983727ebed..aefb1b663d8 100644 --- a/internal-api/src/main/java/datadog/trace/api/iast/VulnerabilityTypes.java +++ b/internal-api/src/main/java/datadog/trace/api/iast/VulnerabilityTypes.java @@ -37,6 +37,7 @@ private VulnerabilityTypes() {} public static final byte SESSION_REWRITING = 28; public static final byte DEFAULT_APP_DEPLOYED = 29; public static final byte UNTRUSTED_DESERIALIZATION = 30; + public static final byte EMAIL_HTML_INJECTION = 31; /** * Use for telemetry only, this is a special vulnerability type that is not reported, reported @@ -115,7 +116,8 @@ private VulnerabilityTypes() {} "REFLECTION_INJECTION", "SESSION_REWRITING", "DEFAULT_APP_DEPLOYED", - "UNTRUSTED_DESERIALIZATION" + "UNTRUSTED_DESERIALIZATION", + "EMAIL_HTML_INJECTION" }; public static String toString(final byte vulnerability) { diff --git a/internal-api/src/main/java/datadog/trace/api/iast/sink/EmailInjectionModule.java b/internal-api/src/main/java/datadog/trace/api/iast/sink/EmailInjectionModule.java new file mode 100644 index 00000000000..53a81d507bd --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/iast/sink/EmailInjectionModule.java @@ -0,0 +1,8 @@ +package datadog.trace.api.iast.sink; + +import datadog.trace.api.iast.IastModule; +import javax.annotation.Nullable; + +public interface EmailInjectionModule extends IastModule { + void onSendEmail(@Nullable Object message); +} diff --git a/internal-api/src/test/groovy/datadog/trace/api/iast/VulnerabilityTypesTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/iast/VulnerabilityTypesTest.groovy index a7c9e2e23f6..90f93548144 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/iast/VulnerabilityTypesTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/iast/VulnerabilityTypesTest.groovy @@ -45,5 +45,6 @@ class VulnerabilityTypesTest extends DDSpecification { VulnerabilityTypes.SESSION_REWRITING | 'SESSION_REWRITING' VulnerabilityTypes.DEFAULT_APP_DEPLOYED | 'DEFAULT_APP_DEPLOYED' VulnerabilityTypes.UNTRUSTED_DESERIALIZATION | 'UNTRUSTED_DESERIALIZATION' + VulnerabilityTypes.EMAIL_HTML_INJECTION | 'EMAIL_HTML_INJECTION' } } diff --git a/settings.gradle b/settings.gradle index 719f7d1efaf..f6082cc6b63 100644 --- a/settings.gradle +++ b/settings.gradle @@ -282,6 +282,7 @@ include ':dd-java-agent:instrumentation:jackson-core:jackson-core-2.12' include ':dd-java-agent:instrumentation:jackson-core:jackson-core-2.16' include ':dd-java-agent:instrumentation:jacoco' include ':dd-java-agent:instrumentation:jakarta-jms' +include ':dd-java-agent:instrumentation:jakarta-mail' include ':dd-java-agent:instrumentation:jakarta-rs-annotations-3' include ':dd-java-agent:instrumentation:jakarta-ws-annotations' include ':dd-java-agent:instrumentation:java-concurrent' @@ -301,6 +302,7 @@ include ':dd-java-agent:instrumentation:java-security' include ':dd-java-agent:instrumentation:java-util' include ':dd-java-agent:instrumentation:javax-naming' include ':dd-java-agent:instrumentation:javax-xml' +include ':dd-java-agent:instrumentation:javax-mail' include ':dd-java-agent:instrumentation:jax-rs-annotations-1' include ':dd-java-agent:instrumentation:jax-rs-annotations-2' include ':dd-java-agent:instrumentation:jax-rs-annotations-2:filter-jersey'