Skip to content

Commit 9ec7fde

Browse files
sezen-datadogjandro996smolaMariovido
authored
Email HTML Injection detection in IAST (#8205)
* 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]>
1 parent 41a2e99 commit 9ec7fde

File tree

32 files changed

+885
-16
lines changed

32 files changed

+885
-16
lines changed

dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.datadog.iast.securitycontrol.IastSecurityControlTransformer;
1212
import com.datadog.iast.sink.ApplicationModuleImpl;
1313
import com.datadog.iast.sink.CommandInjectionModuleImpl;
14+
import com.datadog.iast.sink.EmailInjectionModuleImpl;
1415
import com.datadog.iast.sink.HardcodedSecretModuleImpl;
1516
import com.datadog.iast.sink.HeaderInjectionModuleImpl;
1617
import com.datadog.iast.sink.HstsMissingHeaderModuleImpl;
@@ -179,7 +180,8 @@ private static Stream<IastModule> iastModules(
179180
HardcodedSecretModuleImpl.class,
180181
InsecureAuthProtocolModuleImpl.class,
181182
ReflectionInjectionModuleImpl.class,
182-
UntrustedDeserializationModuleImpl.class);
183+
UntrustedDeserializationModuleImpl.class,
184+
EmailInjectionModuleImpl.class);
183185
if (iast != FULLY_ENABLED) {
184186
modules = modules.filter(IastSystem::isOptOut);
185187
}

dd-java-agent/agent-iast/src/main/java/com/datadog/iast/model/VulnerabilityType.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static com.datadog.iast.util.CRCUtils.update;
44
import static datadog.trace.api.iast.VulnerabilityMarks.COMMAND_INJECTION_MARK;
5+
import static datadog.trace.api.iast.VulnerabilityMarks.EMAIL_HTML_INJECTION_MARK;
56
import static datadog.trace.api.iast.VulnerabilityMarks.HEADER_INJECTION_MARK;
67
import static datadog.trace.api.iast.VulnerabilityMarks.LDAP_INJECTION_MARK;
78
import static datadog.trace.api.iast.VulnerabilityMarks.NOT_MARKED;
@@ -164,6 +165,12 @@ public interface VulnerabilityType {
164165
.excludedSources(Builder.DB_EXCLUDED)
165166
.build();
166167

168+
VulnerabilityType EMAIL_HTML_INJECTION =
169+
type(VulnerabilityTypes.EMAIL_HTML_INJECTION)
170+
.mark(EMAIL_HTML_INJECTION_MARK)
171+
.excludedSources(Builder.DB_EXCLUDED)
172+
.build();
173+
167174
/* All vulnerability types that have a mark. Should be updated if new vulnerabilityType with mark is added */
168175
VulnerabilityType[] MARKED_VULNERABILITIES = {
169176
SQL_INJECTION,
@@ -177,7 +184,8 @@ public interface VulnerabilityType {
177184
XSS,
178185
HEADER_INJECTION,
179186
REFLECTION_INJECTION,
180-
UNTRUSTED_DESERIALIZATION
187+
UNTRUSTED_DESERIALIZATION,
188+
EMAIL_HTML_INJECTION
181189
};
182190

183191
String name();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.datadog.iast.sink;
2+
3+
import com.datadog.iast.Dependencies;
4+
import com.datadog.iast.model.VulnerabilityType;
5+
import datadog.trace.api.iast.sink.EmailInjectionModule;
6+
import javax.annotation.Nullable;
7+
8+
public class EmailInjectionModuleImpl extends SinkModuleBase implements EmailInjectionModule {
9+
public EmailInjectionModuleImpl(final Dependencies dependencies) {
10+
super(dependencies);
11+
}
12+
13+
@Override
14+
public void onSendEmail(@Nullable final Object messageContent) {
15+
if (messageContent == null) {
16+
return;
17+
}
18+
checkInjection(VulnerabilityType.EMAIL_HTML_INJECTION, messageContent);
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.datadog.iast.sink
2+
3+
import com.datadog.iast.IastModuleImplTestBase
4+
import com.datadog.iast.Reporter
5+
import com.datadog.iast.model.Vulnerability
6+
import com.datadog.iast.model.VulnerabilityType
7+
import com.datadog.iast.propagation.PropagationModuleImpl
8+
import datadog.trace.api.iast.SourceTypes
9+
10+
class EmailInjectionModuleTest extends IastModuleImplTestBase{
11+
12+
private EmailInjectionModuleImpl module
13+
14+
def setup() {
15+
module = new EmailInjectionModuleImpl(dependencies)
16+
}
17+
18+
@Override
19+
protected Reporter buildReporter() {
20+
return Mock(Reporter)
21+
}
22+
23+
def "test onSendEmail with null messageContent"() {
24+
when:
25+
module.onSendEmail(null)
26+
27+
then:
28+
noExceptionThrown()
29+
}
30+
31+
def "test onSendEmail with non-null messageContent"() {
32+
given:
33+
def messageContent = "test message"
34+
def propagationModule = new PropagationModuleImpl()
35+
propagationModule.taintObject(messageContent, SourceTypes.NONE)
36+
37+
when:
38+
module.onSendEmail(messageContent)
39+
40+
then:
41+
1 * reporter.report(_, _) >> { args ->
42+
def vulnerability = args[1] as Vulnerability
43+
vulnerability.type == VulnerabilityType.EMAIL_HTML_INJECTION &&
44+
vulnerability.evidence == messageContent
45+
}
46+
}
47+
}

dd-java-agent/instrumentation/commons-lang-2/src/main/java/datadog/trace/instrumentation/commonslang/StringEscapeUtilsCallSite.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public static String afterEscape(
2525
final PropagationModule module = InstrumentationBridge.PROPAGATION;
2626
if (module != null) {
2727
try {
28-
module.taintStringIfTainted(result, input, false, VulnerabilityMarks.XSS_MARK);
28+
module.taintStringIfTainted(result, input, false, VulnerabilityMarks.HTML_ESCAPED_MARK);
2929
} catch (final Throwable e) {
3030
module.onUnexpectedException("afterEscape threw", e);
3131
}

dd-java-agent/instrumentation/commons-lang-2/src/test/groovy/datadog/trace/instrumentation/commonslang/StringEscapeUtilsCallSiteTest.groovy

+25-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import groovy.transform.CompileDynamic
88

99
import static datadog.trace.api.iast.VulnerabilityMarks.SQL_INJECTION_MARK
1010
import static datadog.trace.api.iast.VulnerabilityMarks.XSS_MARK
11+
import static datadog.trace.api.iast.VulnerabilityMarks.EMAIL_HTML_INJECTION_MARK
1112

1213
@CompileDynamic
1314
class StringEscapeUtilsCallSiteTest extends AgentTestRunner {
@@ -27,15 +28,32 @@ class StringEscapeUtilsCallSiteTest extends AgentTestRunner {
2728

2829
then:
2930
result == expected
30-
1 * module.taintStringIfTainted(_ as String, args[0], false, mark)
31+
1 * module.taintStringIfTainted(_ as String, args[0], false, XSS_MARK | EMAIL_HTML_INJECTION_MARK)
3132
0 * _
3233

3334
where:
34-
method | args | mark | expected
35-
'escapeHtml' | ['Ø-This is a quote'] | XSS_MARK | '&Oslash;-This is a quote'
36-
'escapeJava' | ['Ø-This is a quote'] | XSS_MARK | '\\u00D8-This is a quote'
37-
'escapeJavaScript' | ['Ø-This is a quote'] | XSS_MARK | '\\u00D8-This is a quote'
38-
'escapeXml' | ['Ø-This is a quote'] | XSS_MARK | '&#216;-This is a quote'
39-
'escapeSql' | ['Ø-This is a quote'] | SQL_INJECTION_MARK | 'Ø-This is a quote'
35+
method | args | expected
36+
'escapeHtml' | ['Ø-This is a quote'] | '&Oslash;-This is a quote'
37+
'escapeJava' | ['Ø-This is a quote'] | '\\u00D8-This is a quote'
38+
'escapeJavaScript' | ['Ø-This is a quote'] | '\\u00D8-This is a quote'
39+
'escapeXml' | ['Ø-This is a quote'] | '&#216;-This is a quote'
40+
}
41+
42+
void 'test #method sql'() {
43+
given:
44+
final module = Mock(PropagationModule)
45+
InstrumentationBridge.registerIastModule(module)
46+
47+
when:
48+
final result = TestStringEscapeUtilsSuite.&"$method".call(args)
49+
50+
then:
51+
result == expected
52+
1 * module.taintStringIfTainted(_ as String, args[0], false, SQL_INJECTION_MARK)
53+
0 * _
54+
55+
where:
56+
method | args | expected
57+
'escapeSql' | ['Ø-This is a quote'] | 'Ø-This is a quote'
4058
}
4159
}

dd-java-agent/instrumentation/commons-lang-3/src/main/java/datadog/trace/instrumentation/commonslang3/StringEscapeUtilsCallSite.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public static String afterEscape(
2727
final PropagationModule module = InstrumentationBridge.PROPAGATION;
2828
if (module != null) {
2929
try {
30-
module.taintStringIfTainted(result, input, false, VulnerabilityMarks.XSS_MARK);
30+
module.taintStringIfTainted(result, input, false, VulnerabilityMarks.HTML_ESCAPED_MARK);
3131
} catch (final Throwable e) {
3232
module.onUnexpectedException("afterEscape threw", e);
3333
}

dd-java-agent/instrumentation/commons-lang-3/src/test/groovy/datadog/trace/instrumentation/commonslang3/StringEscapeUtilsCallSiteTest.groovy

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class StringEscapeUtilsCallSiteTest extends AgentTestRunner {
2525

2626
then:
2727
result == expected
28-
1 * module.taintStringIfTainted(_ as String, args[0], false, VulnerabilityMarks.XSS_MARK)
28+
1 * module.taintStringIfTainted(_ as String, args[0], false, VulnerabilityMarks.HTML_ESCAPED_MARK)
2929
0 * _
3030

3131
where:

dd-java-agent/instrumentation/commons-text/src/main/java/datadog/trace/instrumentation/commonstext/StringEscapeUtilsCallSite.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public static String afterEscape(
2929
final PropagationModule module = InstrumentationBridge.PROPAGATION;
3030
if (module != null) {
3131
try {
32-
module.taintStringIfTainted(result, input, false, VulnerabilityMarks.XSS_MARK);
32+
module.taintStringIfTainted(result, input, false, VulnerabilityMarks.HTML_ESCAPED_MARK);
3333
} catch (final Throwable e) {
3434
module.onUnexpectedException("afterEscape threw", e);
3535
}

dd-java-agent/instrumentation/commons-text/src/test/groovy/datadog/trace/instrumentation/commonstext/StringEscapeUtilsCallSiteTest.groovy

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class StringEscapeUtilsCallSiteTest extends AgentTestRunner {
2525

2626
then:
2727
result == expected
28-
1 * module.taintStringIfTainted(_ as String, args[0], false, VulnerabilityMarks.XSS_MARK)
28+
1 * module.taintStringIfTainted(_ as String, args[0], false, VulnerabilityMarks.HTML_ESCAPED_MARK)
2929
0 * _
3030

3131
where:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
muzzle {
2+
pass {
3+
group = 'jakarta.mail'
4+
module = 'jakarta.mail-api'
5+
versions = '[2.0.1, ]'
6+
}
7+
}
8+
9+
apply from: "$rootDir/gradle/java.gradle"
10+
11+
addTestSuiteForDir('latestDepTest', 'test')
12+
13+
dependencies {
14+
testRuntimeOnly project(':dd-java-agent:instrumentation:iast-instrumenter')
15+
compileOnly 'jakarta.mail:jakarta.mail-api:2.0.1'
16+
testImplementation 'jakarta.mail:jakarta.mail-api:2.0.1'
17+
compileOnly 'com.sun.mail:jakarta.mail:2.0.1'
18+
testImplementation 'com.sun.mail:jakarta.mail:2.0.1'
19+
compileOnly 'jakarta.activation:jakarta.activation-api:2.0.1'
20+
testImplementation 'jakarta.activation:jakarta.activation-api:2.0.1'
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package datadog.trace.instrumentation.jakarta.mail;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
4+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
5+
6+
import com.google.auto.service.AutoService;
7+
import datadog.trace.agent.tooling.Instrumenter;
8+
import datadog.trace.agent.tooling.InstrumenterModule;
9+
import datadog.trace.api.iast.InstrumentationBridge;
10+
import datadog.trace.api.iast.Sink;
11+
import datadog.trace.api.iast.VulnerabilityTypes;
12+
import datadog.trace.api.iast.sink.EmailInjectionModule;
13+
import jakarta.mail.Message;
14+
import jakarta.mail.MessagingException;
15+
import jakarta.mail.Multipart;
16+
import jakarta.mail.Part;
17+
import java.io.IOException;
18+
import net.bytebuddy.asm.Advice;
19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
22+
@AutoService(InstrumenterModule.class)
23+
public class JakartaMailInstrumentation extends InstrumenterModule.Iast
24+
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
25+
26+
private static Logger LOGGER = LoggerFactory.getLogger(JakartaMailInstrumentation.class);
27+
28+
public JakartaMailInstrumentation() {
29+
super("jakarta-mail", "jakarta-mail-transport");
30+
}
31+
32+
@Override
33+
public void methodAdvice(MethodTransformer transformer) {
34+
transformer.applyAdvice(
35+
named("send0").and(takesArgument(0, named("jakarta.mail.Message"))),
36+
JakartaMailInstrumentation.class.getName() + "$MailInjectionAdvice");
37+
}
38+
39+
@Override
40+
public String instrumentedType() {
41+
return "jakarta.mail.Transport";
42+
}
43+
44+
public static class MailInjectionAdvice {
45+
@Sink(VulnerabilityTypes.EMAIL_HTML_INJECTION)
46+
@Advice.OnMethodEnter(suppress = Throwable.class)
47+
public static void onSend(@Advice.Argument(0) final Message message)
48+
throws MessagingException, IOException {
49+
EmailInjectionModule emailInjectionModule = InstrumentationBridge.EMAIL_INJECTION;
50+
if (emailInjectionModule == null) {
51+
return;
52+
}
53+
if (message == null || message.getContent() == null) {
54+
return;
55+
}
56+
if (message.isMimeType("text/html")) {
57+
emailInjectionModule.onSendEmail(message.getContent());
58+
} else if (message.isMimeType("multipart/*")) {
59+
Multipart parts = (Multipart) message.getContent();
60+
for (int i = 0; i < parts.getCount(); i++) {
61+
final Part part = parts.getBodyPart(i);
62+
if (part != null && part.isMimeType("text/html")) {
63+
emailInjectionModule.onSendEmail(part.getContent());
64+
}
65+
}
66+
}
67+
}
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package datadog.trace.instrumentation.jakarta.mail;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface;
4+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
5+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
6+
7+
import com.google.auto.service.AutoService;
8+
import datadog.trace.agent.tooling.Instrumenter;
9+
import datadog.trace.agent.tooling.InstrumenterModule;
10+
import datadog.trace.api.iast.InstrumentationBridge;
11+
import datadog.trace.api.iast.Propagation;
12+
import datadog.trace.api.iast.propagation.PropagationModule;
13+
import jakarta.mail.Part;
14+
import net.bytebuddy.asm.Advice;
15+
import net.bytebuddy.description.type.TypeDescription;
16+
import net.bytebuddy.matcher.ElementMatcher;
17+
18+
@AutoService(InstrumenterModule.class)
19+
public class JakartaMailPartInstrumentation extends InstrumenterModule.Iast
20+
implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice {
21+
22+
public JakartaMailPartInstrumentation() {
23+
super("jakarta-mail", "jakarta-mail-body");
24+
}
25+
26+
@Override
27+
public void methodAdvice(MethodTransformer transformer) {
28+
transformer.applyAdvice(
29+
named("setContent").and(takesArgument(0, Object.class)),
30+
JakartaMailPartInstrumentation.class.getName() + "$ContentInjectionAdvice");
31+
transformer.applyAdvice(
32+
named("setText").and(takesArgument(0, String.class)),
33+
JakartaMailPartInstrumentation.class.getName() + "$TextInjectionAdvice");
34+
}
35+
36+
@Override
37+
public String hierarchyMarkerType() {
38+
return "jakarta.mail.Part";
39+
}
40+
41+
@Override
42+
public ElementMatcher<TypeDescription> hierarchyMatcher() {
43+
return implementsInterface(named(hierarchyMarkerType()));
44+
}
45+
46+
public static class ContentInjectionAdvice {
47+
@Propagation
48+
@Advice.OnMethodEnter(suppress = Throwable.class)
49+
private static void onSetContent(
50+
@Advice.This Part part, @Advice.Argument(0) final Object content) {
51+
PropagationModule propagationModule = InstrumentationBridge.PROPAGATION;
52+
if (propagationModule != null && content != null) {
53+
propagationModule.taintObjectIfTainted(part, content);
54+
}
55+
}
56+
}
57+
58+
public static class TextInjectionAdvice {
59+
@Propagation
60+
@Advice.OnMethodEnter(suppress = Throwable.class)
61+
private static void onSetText(@Advice.This Part part, @Advice.Argument(0) final String text) {
62+
PropagationModule propagationModule = InstrumentationBridge.PROPAGATION;
63+
if (propagationModule != null && text != null) {
64+
propagationModule.taintObjectIfTainted(part, text);
65+
}
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)