Skip to content

Commit

Permalink
Email HTML Injection detection in IAST (#8205)
Browse files Browse the repository at this point in the history
* Email Injection detection in IAST

---------

Co-authored-by: Alejandro González García <[email protected]>
Co-authored-by: Santiago M. Mola <[email protected]>
Co-authored-by: Mario Vidal Domínguez <[email protected]>
  • Loading branch information
4 people authored Feb 5, 2025
1 parent 41a2e99 commit 9ec7fde
Show file tree
Hide file tree
Showing 32 changed files with 885 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -179,7 +180,8 @@ private static Stream<IastModule> iastModules(
HardcodedSecretModuleImpl.class,
InsecureAuthProtocolModuleImpl.class,
ReflectionInjectionModuleImpl.class,
UntrustedDeserializationModuleImpl.class);
UntrustedDeserializationModuleImpl.class,
EmailInjectionModuleImpl.class);
if (iast != FULLY_ENABLED) {
modules = modules.filter(IastSystem::isOptOut);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -177,7 +184,8 @@ public interface VulnerabilityType {
XSS,
HEADER_INJECTION,
REFLECTION_INJECTION,
UNTRUSTED_DESERIALIZATION
UNTRUSTED_DESERIALIZATION,
EMAIL_HTML_INJECTION
};

String name();
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 | '&Oslash;-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 | '&#216;-This is a quote'
'escapeSql' | ['Ø-This is a quote'] | SQL_INJECTION_MARK | 'Ø-This is a quote'
method | args | expected
'escapeHtml' | ['Ø-This is a quote'] | '&Oslash;-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'] | '&#216;-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'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 21 additions & 0 deletions dd-java-agent/instrumentation/jakarta-mail/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<TypeDescription> 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);
}
}
}
}
Loading

0 comments on commit 9ec7fde

Please sign in to comment.