Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Email HTML Injection detection in IAST #8205

Open
wants to merge 72 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
508a671
Email Injection detection in IAST
sezen-datadog Jan 15, 2025
a0f62f4
email injection checks
sezen-datadog Jan 15, 2025
59ea624
instrumentation class put in place
sezen-datadog Jan 15, 2025
14df382
EMAIL_HTML_INJECTION
sezen-datadog Jan 15, 2025
700dd63
pr comments easy ones
sezen-datadog Jan 15, 2025
b4225d2
only focus on transport send
sezen-datadog Jan 15, 2025
34ac9fb
pr comments
sezen-datadog Jan 15, 2025
bcca415
first attempt at instrumentation
sezen-datadog Jan 15, 2025
4b9c23c
correction on argument
sezen-datadog Jan 15, 2025
14458ed
Update dd-java-agent/instrumentation/javax-mail/src/main/java/datadog…
sezen-datadog Jan 16, 2025
b548721
Update dd-java-agent/instrumentation/javax-mail/src/main/java/datadog…
sezen-datadog Jan 16, 2025
4182134
advice class added for easier debugging
sezen-datadog Jan 16, 2025
791a5fa
html escapes with vulnerability mark
sezen-datadog Jan 16, 2025
cd9f249
instrumentation skeleton
sezen-datadog Jan 17, 2025
74cebb5
instrumentation of body elements
sezen-datadog Jan 17, 2025
ea76961
instrumentation of body elements
sezen-datadog Jan 17, 2025
e98038d
test start
sezen-datadog Jan 17, 2025
2111e77
test continue
sezen-datadog Jan 17, 2025
b814fd0
test continue
sezen-datadog Jan 17, 2025
460737d
test continue
sezen-datadog Jan 17, 2025
be50dc3
test OK
sezen-datadog Jan 20, 2025
fba4788
define the tests I want
sezen-datadog Jan 20, 2025
5fb78f1
content test OK
sezen-datadog Jan 20, 2025
a1ab334
content test OK
sezen-datadog Jan 20, 2025
d64327a
content test OK
sezen-datadog Jan 20, 2025
6f8e74f
content test OK
sezen-datadog Jan 20, 2025
49291c1
smoke test controller
sezen-datadog Jan 21, 2025
bbf5486
smoke test controller
sezen-datadog Jan 21, 2025
ee69fd5
smoke test
sezen-datadog Jan 21, 2025
ac708fe
smoke test
sezen-datadog Jan 21, 2025
ba2da19
Merge branch 'master' into sezen.leblay/APPSEC-56330-email-injection
sezen-datadog Jan 21, 2025
553c7d8
Update dd-java-agent/instrumentation/javax-mail/src/main/java/datadog…
sezen-datadog Jan 21, 2025
e3eaf20
Update dd-java-agent/instrumentation/javax-mail/src/main/java/datadog…
sezen-datadog Jan 21, 2025
95248a2
pr
sezen-datadog Jan 21, 2025
4993aec
pr
sezen-datadog Jan 21, 2025
4883918
pr
sezen-datadog Jan 21, 2025
56b3521
build correction
sezen-datadog Jan 22, 2025
cb9a54f
Update dd-java-agent/agent-iast/src/main/java/com/datadog/iast/model/…
sezen-datadog Jan 22, 2025
482a231
build correction
sezen-datadog Jan 22, 2025
69044c0
Merge branch 'master' into sezen.leblay/APPSEC-56330-email-injection
sezen-datadog Jan 22, 2025
bccb5ae
build correction
sezen-datadog Jan 22, 2025
990bbb7
Merge branch 'master' into sezen.leblay/APPSEC-56330-email-injection
sezen-datadog Jan 22, 2025
6727eff
pr
sezen-datadog Jan 22, 2025
6337ba5
pr
sezen-datadog Jan 22, 2025
b8595c7
pr
sezen-datadog Jan 22, 2025
3384382
build
sezen-datadog Jan 22, 2025
c9895be
build
sezen-datadog Jan 27, 2025
3f78b54
build
sezen-datadog Jan 27, 2025
7950096
unit tests pass
sezen-datadog Jan 27, 2025
f6da333
de sally no more
sezen-datadog Jan 27, 2025
f9b7617
de sally no more
sezen-datadog Jan 27, 2025
2ff2278
smoke tests
sezen-datadog Jan 27, 2025
a8b13ca
smoke tests
sezen-datadog Jan 27, 2025
c54f206
smoke tests
sezen-datadog Jan 28, 2025
4fdb7f7
Merge branch 'master' into sezen.leblay/APPSEC-56330-email-injection
sezen-datadog Jan 28, 2025
28a67ba
muzzle
sezen-datadog Jan 28, 2025
2e7468c
manu's suggestions
sezen-datadog Jan 28, 2025
59e42e0
beautify
sezen-datadog Jan 28, 2025
845b2d0
EMAIL_HTML_INJECTION instead of EMAIL_INJECTION
sezen-datadog Jan 29, 2025
34796da
EMAIL_HTML_INJECTION instead of EMAIL_INJECTION
sezen-datadog Jan 29, 2025
631b775
pr
sezen-datadog Jan 31, 2025
86321f6
Merge branch 'master' into sezen.leblay/APPSEC-56330-email-injection
sezen-datadog Feb 3, 2025
42b355d
unit test correction - propagation fails though
sezen-datadog Feb 3, 2025
aedcb1e
activation dependencies added
sezen-datadog Feb 3, 2025
f0aa378
test
sezen-datadog Feb 3, 2025
457e9f6
tests
sezen-datadog Feb 4, 2025
aac7112
Merge branch 'master' into sezen.leblay/APPSEC-56330-email-injection
sezen-datadog Feb 4, 2025
8b17208
javax removed from smoke tests (cant have both javax + jakarta)
sezen-datadog Feb 4, 2025
3f9815d
oops
sezen-datadog Feb 4, 2025
73b3cb1
oops
sezen-datadog Feb 4, 2025
bc708c5
PR
sezen-datadog Feb 5, 2025
943227d
Merge branch 'master' into sezen.leblay/APPSEC-56330-email-injection
sezen-datadog Feb 5, 2025
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 @@ -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'() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we cannot reuse the previous test and just add a new mark?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because the mark being tested is EMAIL | XSS and I could not figure out how to escape the pipe character for the test lol

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can do a workaround with this:

  • given:
    final combinedMarks = marks.inject(0) { acc, mark -> acc | mark }
  • then:
    1 * module.taintStringIfTainted(_ as String, args[0], false, combinedMarks)
  • where:
    method             | args                  | expected                  | marks
    'escapeJavaScript' | ['Ø-This is a quote'] | '\\u00D8-This is a quote' | [XSS_MARK, EMAIL_HTML_INJECTION_MARK]
    'escapeSql'        | ['Ø-This is a quote'] | 'Ø-This is a quote'       | [SQL_INJECTION_MARK]

As we want to do a bitwise operation we can pass an array and do the bitwise operation inside the test. In my opinion in this way the tests are easier to follow than having them splitted. Nonetheless, if you disagree, you can leave it as it is as it is not a real issue :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might as well do separate tests for separate marks, i think

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
Loading