Skip to content

Commit 09f74df

Browse files
committed
#411: expose validation sub steps in the MailerHelper class for the completeness check, CRLF inject scans and address validations
1 parent cad2b7a commit 09f74df

File tree

5 files changed

+121
-35
lines changed

5 files changed

+121
-35
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.simplejavamail.mailer;
2+
3+
import org.simplejavamail.MailException;
4+
5+
public class MailCompletenessException extends MailValidationException {
6+
7+
static final String MISSING_SENDER = "Email is not valid: missing sender. Provide with emailBuilder.from(...)";
8+
static final String MISSING_RECIPIENT = "Email is not valid: missing recipients";
9+
static final String MISSING_DISPOSITIONNOTIFICATIONTO = "Email is not valid: it is set to use \"Disposition Notification To\", but the address is empty";
10+
static final String MISSING_RETURNRECEIPTTO = "Email is not valid: it is set to use \"Return Receipt To\", but the address is empty";
11+
static final String INJECTION_SUSPECTED = "Suspected of injection attack, field: %s with suspicious value: %s";
12+
13+
14+
MailCompletenessException(final String message) {
15+
super(message);
16+
}
17+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.simplejavamail.mailer;
2+
3+
public class MailInvalidAddressException extends MailValidationException {
4+
5+
static final String INVALID_RECIPIENT = "Invalid TO address: %s";
6+
static final String INVALID_REPLYTO = "Invalid REPLY TO address: %s";
7+
static final String INVALID_BOUNCETO = "Invalid BOUNCE TO address: %s";
8+
static final String INVALID_SENDER = "Invalid FROM address: %s";
9+
static final String INVALID_DISPOSITIONNOTIFICATIONTO = "Invalid \"Disposition Notification To\" address: %s";
10+
static final String INVALID_RETURNRECEIPTTO = "Invalid \"Return Receipt To\" address: %s";
11+
12+
13+
MailInvalidAddressException(final String message) {
14+
super(message);
15+
}
16+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.simplejavamail.mailer;
2+
3+
public class MailSuspiciousCRLFValueException extends MailValidationException {
4+
5+
static final String INJECTION_SUSPECTED = "Suspected of injection attack, field: %s with suspicious value: %s";
6+
7+
8+
MailSuspiciousCRLFValueException(final String message) {
9+
super(message);
10+
}
11+
}

modules/simple-java-mail/src/main/java/org/simplejavamail/mailer/MailValidationException.java

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,9 @@
22

33
import org.simplejavamail.MailException;
44

5-
class MailValidationException extends MailException {
6-
7-
static final String INVALID_RECIPIENT = "Invalid TO address: %s";
8-
static final String INVALID_REPLYTO = "Invalid REPLY TO address: %s";
9-
static final String INVALID_BOUNCETO = "Invalid BOUNCE TO address: %s";
10-
static final String INVALID_SENDER = "Invalid FROM address: %s";
11-
static final String INVALID_DISPOSITIONNOTIFICATIONTO = "Invalid \"Disposition Notification To\" address: %s";
12-
static final String INVALID_RETURNRECEIPTTO = "Invalid \"Return Receipt To\" address: %s";
13-
static final String MISSING_SENDER = "Email is not valid: missing sender. Provide with emailBuilder.from(...)";
14-
static final String MISSING_RECIPIENT = "Email is not valid: missing recipients";
15-
static final String MISSING_DISPOSITIONNOTIFICATIONTO = "Email is not valid: it is set to use \"Disposition Notification To\", but the address is empty";
16-
static final String MISSING_RETURNRECEIPTTO = "Email is not valid: it is set to use \"Return Receipt To\", but the address is empty";
17-
static final String INJECTION_SUSPECTED = "Suspected of injection attack, field: %s with suspicious value: %s";
18-
5+
public abstract class MailValidationException extends MailException {
196

207
MailValidationException(final String message) {
218
super(message);
229
}
23-
}
10+
}

modules/simple-java-mail/src/main/java/org/simplejavamail/mailer/MailerHelper.java

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,55 +30,114 @@ public class MailerHelper {
3030

3131
private static final Logger LOGGER = getLogger(MailerHelper.class);
3232

33+
/**
34+
* Delegates to all other validations for a full checkup.
35+
*
36+
* @see #validateCompleteness(Email)
37+
* @see #validateAddresses(Email, EmailValidator)
38+
* @see #scanForInjectionAttacks(Email)
39+
*/
3340
@SuppressWarnings({ "SameReturnValue" })
3441
public static boolean validate(@NotNull final Email email, @Nullable final EmailValidator emailValidator)
3542
throws MailException {
3643
LOGGER.debug("validating email...");
3744

45+
validateCompleteness(email);
46+
validateAddresses(email, emailValidator);
47+
scanForInjectionAttacks(email);
48+
49+
LOGGER.debug("...no problems found");
50+
51+
return true;
52+
}
53+
54+
/**
55+
* Checks whether:
56+
* <ol>
57+
* <li>there are recipients</li>
58+
* <li>if there is a sender</li>
59+
* <li>if there is a disposition notification TO if flag is set to use it</li>
60+
* <li>if there is a return receipt TO if flag is set to use it</li>
61+
* </ol>
62+
*/
63+
public static void validateCompleteness(final @NotNull Email email) {
3864
// check for mandatory values
3965
if (email.getRecipients().size() == 0) {
40-
throw new MailValidationException(MailValidationException.MISSING_RECIPIENT);
66+
throw new MailCompletenessException(MailCompletenessException.MISSING_RECIPIENT);
4167
} else if (email.getFromRecipient() == null) {
42-
throw new MailValidationException(MailValidationException.MISSING_SENDER);
68+
throw new MailCompletenessException(MailCompletenessException.MISSING_SENDER);
4369
} else if (email.isUseDispositionNotificationTo() && email.getDispositionNotificationTo() == null) {
44-
throw new MailValidationException(MailValidationException.MISSING_DISPOSITIONNOTIFICATIONTO);
70+
throw new MailCompletenessException(MailCompletenessException.MISSING_DISPOSITIONNOTIFICATIONTO);
4571
} else if (email.isUseReturnReceiptTo() && email.getReturnReceiptTo() == null) {
46-
throw new MailValidationException(MailValidationException.MISSING_RETURNRECEIPTTO);
47-
} else
72+
throw new MailCompletenessException(MailCompletenessException.MISSING_RETURNRECEIPTTO);
73+
}
74+
}
75+
76+
/**
77+
* If email validator is provided, checks:
78+
* <ol>
79+
* <li>from recipient</li>
80+
* <li>all TO/CC/BCC recipients</li>
81+
* <li>reply-to recipient, if provided</li>
82+
* <li>bounce-to recipient, if provided</li>
83+
* <li>disposition-notification-to recipient, if provided</li>
84+
* <li>return-receipt-to recipient, if provided</li>
85+
* </ol>
86+
*/
87+
public static void validateAddresses(final @NotNull Email email, final @Nullable EmailValidator emailValidator) {
4888
if (emailValidator != null) {
4989
if (!emailValidator.isValid(email.getFromRecipient().getAddress())) {
50-
throw new MailValidationException(format(MailValidationException.INVALID_SENDER, email));
90+
throw new MailInvalidAddressException(format(MailInvalidAddressException.INVALID_SENDER, email));
5191
}
5292
for (final Recipient recipient : email.getRecipients()) {
5393
if (!emailValidator.isValid(recipient.getAddress())) {
54-
throw new MailValidationException(format(MailValidationException.INVALID_RECIPIENT, email));
94+
throw new MailInvalidAddressException(format(MailInvalidAddressException.INVALID_RECIPIENT, email));
5595
}
5696
}
5797
if (email.getReplyToRecipient() != null && !emailValidator.isValid(email.getReplyToRecipient().getAddress())) {
58-
throw new MailValidationException(format(MailValidationException.INVALID_REPLYTO, email));
98+
throw new MailInvalidAddressException(format(MailInvalidAddressException.INVALID_REPLYTO, email));
5999
}
60100
if (email.getBounceToRecipient() != null && !emailValidator.isValid(email.getBounceToRecipient().getAddress())) {
61-
throw new MailValidationException(format(MailValidationException.INVALID_BOUNCETO, email));
101+
throw new MailInvalidAddressException(format(MailInvalidAddressException.INVALID_BOUNCETO, email));
62102
}
63103
if (email.isUseDispositionNotificationTo()) {
64104
if (!emailValidator.isValid(checkNonEmptyArgument(email.getDispositionNotificationTo(), "dispositionNotificationTo").getAddress())) {
65-
throw new MailValidationException(format(MailValidationException.INVALID_DISPOSITIONNOTIFICATIONTO, email));
105+
throw new MailInvalidAddressException(format(MailInvalidAddressException.INVALID_DISPOSITIONNOTIFICATIONTO, email));
66106
}
67107
}
68108
if (email.isUseReturnReceiptTo()) {
69109
if (!emailValidator.isValid(checkNonEmptyArgument(email.getReturnReceiptTo(), "returnReceiptTo").getAddress())) {
70-
throw new MailValidationException(format(MailValidationException.INVALID_RETURNRECEIPTTO, email));
110+
throw new MailInvalidAddressException(format(MailInvalidAddressException.INVALID_RETURNRECEIPTTO, email));
71111
}
72112
}
73113
}
114+
}
74115

116+
/**
117+
* Checks the following headers for suspicious content (newlines and characters):
118+
* <ol>
119+
* <li>subject</li>
120+
* <li>every header name and value</li>
121+
* <li>every attachment name, nested datasource name and description</li>
122+
* <li>every embedded image name, nested datasource name and description</li>
123+
* <li>from recipient name and address</li>
124+
* <li>replyTo recipient name and address, if provided</li>
125+
* <li>bounceTo recipient name and address, if provided</li>
126+
* <li>every TO/CC/BCC recipient name and address</li>
127+
* <li>disposition-notification-to recipient name and address, if provided</li>
128+
* <li>return-receipt-to recipient name and address, if provided</li>
129+
* </ol>
130+
*
131+
* @see #scanForInjectionAttack
132+
*/
133+
public static void scanForInjectionAttacks(final @NotNull Email email) {
75134
// check for illegal values
76135
scanForInjectionAttack(email.getSubject(), "email.subject");
77136
for (final Map.Entry<String, Collection<String>> headerEntry : email.getHeaders().entrySet()) {
78137
for (final String headerValue : headerEntry.getValue()) {
79138
// FIXME is this still needed?
80139
scanForInjectionAttack(headerEntry.getKey(), "email.header.headerName");
81-
scanForInjectionAttack(MimeUtility.unfold(headerValue), "email.header." + headerEntry.getKey());
140+
scanForInjectionAttack(MimeUtility.unfold(headerValue), format("email.header.[%s]", headerEntry.getKey()));
82141
}
83142
}
84143
for (final AttachmentResource attachment : email.getAttachments()) {
@@ -113,24 +172,20 @@ public static boolean validate(@NotNull final Email email, @Nullable final Email
113172
scanForInjectionAttack(recipient.getName(), "email.recipient.name");
114173
scanForInjectionAttack(recipient.getAddress(), "email.recipient.address");
115174
}
116-
117-
LOGGER.debug("...no problems found");
118-
119-
return true;
120175
}
121176

122177
/**
123-
* @param value Value checked for suspicious newline characters "\n", "\r" and "%0A" (as acknowledged by SMTP servers).
178+
* @param value Value checked for suspicious newline characters "\n", "\r" and the URL-encoded newline "%0A" (as acknowledged by SMTP servers).
124179
* @param valueLabel The name of the field being checked, used for reporting exceptions.
125180
*
126181
* @see <a href="https://web.archive.org/web/20160331233647/http://www.cakesolutions.net/teamblogs/2008/05/08/email-header-injection-security">Email Header Injection security</a>
127182
* @see <a href="https://security.stackexchange.com/a/54100/110048">StackExchange - What threats come from CRLF in email generation?</a>
128183
* @see <a href="https://archive.ph/NuETu">OWASP - Testing for IMAP SMTP Injection</a>
129184
* @see <a href="https://archive.ph/uReuD">CWE-93: Improper Neutralization of CRLF Sequences ('CRLF Injection')</a>
130185
*/
131-
private static void scanForInjectionAttack(final @Nullable String value, final String valueLabel) {
186+
public static void scanForInjectionAttack(final @Nullable String value, final String valueLabel) {
132187
if (value != null && (value.contains("\n") || value.contains("\r") || value.contains("%0A"))) {
133-
throw new MailValidationException(format(MailValidationException.INJECTION_SUSPECTED, valueLabel, value));
188+
throw new MailSuspiciousCRLFValueException(format(MailSuspiciousCRLFValueException.INJECTION_SUSPECTED, valueLabel, value));
134189
}
135190
}
136191

@@ -152,4 +207,4 @@ public static MimeMessage signAndOrEncryptMessageWithSmime(@NotNull final Sessio
152207
return ModuleLoader.loadSmimeModule()
153208
.signAndOrEncryptEmail(session, messageToProtect, emailContainingSmimeDetails, defaultSmimeSigningStore);
154209
}
155-
}
210+
}

0 commit comments

Comments
 (0)