Skip to content

Commit

Permalink
#411: expose validation sub steps in the MailerHelper class for the c…
Browse files Browse the repository at this point in the history
…ompleteness check, CRLF inject scans and address validations
  • Loading branch information
bbottema committed Jul 28, 2022
1 parent cad2b7a commit 09f74df
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.simplejavamail.mailer;

import org.simplejavamail.MailException;

public class MailCompletenessException extends MailValidationException {

static final String MISSING_SENDER = "Email is not valid: missing sender. Provide with emailBuilder.from(...)";
static final String MISSING_RECIPIENT = "Email is not valid: missing recipients";
static final String MISSING_DISPOSITIONNOTIFICATIONTO = "Email is not valid: it is set to use \"Disposition Notification To\", but the address is empty";
static final String MISSING_RETURNRECEIPTTO = "Email is not valid: it is set to use \"Return Receipt To\", but the address is empty";
static final String INJECTION_SUSPECTED = "Suspected of injection attack, field: %s with suspicious value: %s";


MailCompletenessException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.simplejavamail.mailer;

public class MailInvalidAddressException extends MailValidationException {

static final String INVALID_RECIPIENT = "Invalid TO address: %s";
static final String INVALID_REPLYTO = "Invalid REPLY TO address: %s";
static final String INVALID_BOUNCETO = "Invalid BOUNCE TO address: %s";
static final String INVALID_SENDER = "Invalid FROM address: %s";
static final String INVALID_DISPOSITIONNOTIFICATIONTO = "Invalid \"Disposition Notification To\" address: %s";
static final String INVALID_RETURNRECEIPTTO = "Invalid \"Return Receipt To\" address: %s";


MailInvalidAddressException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.simplejavamail.mailer;

public class MailSuspiciousCRLFValueException extends MailValidationException {

static final String INJECTION_SUSPECTED = "Suspected of injection attack, field: %s with suspicious value: %s";


MailSuspiciousCRLFValueException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,9 @@

import org.simplejavamail.MailException;

class MailValidationException extends MailException {

static final String INVALID_RECIPIENT = "Invalid TO address: %s";
static final String INVALID_REPLYTO = "Invalid REPLY TO address: %s";
static final String INVALID_BOUNCETO = "Invalid BOUNCE TO address: %s";
static final String INVALID_SENDER = "Invalid FROM address: %s";
static final String INVALID_DISPOSITIONNOTIFICATIONTO = "Invalid \"Disposition Notification To\" address: %s";
static final String INVALID_RETURNRECEIPTTO = "Invalid \"Return Receipt To\" address: %s";
static final String MISSING_SENDER = "Email is not valid: missing sender. Provide with emailBuilder.from(...)";
static final String MISSING_RECIPIENT = "Email is not valid: missing recipients";
static final String MISSING_DISPOSITIONNOTIFICATIONTO = "Email is not valid: it is set to use \"Disposition Notification To\", but the address is empty";
static final String MISSING_RETURNRECEIPTTO = "Email is not valid: it is set to use \"Return Receipt To\", but the address is empty";
static final String INJECTION_SUSPECTED = "Suspected of injection attack, field: %s with suspicious value: %s";

public abstract class MailValidationException extends MailException {

MailValidationException(final String message) {
super(message);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,55 +30,114 @@ public class MailerHelper {

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

/**
* Delegates to all other validations for a full checkup.
*
* @see #validateCompleteness(Email)
* @see #validateAddresses(Email, EmailValidator)
* @see #scanForInjectionAttacks(Email)
*/
@SuppressWarnings({ "SameReturnValue" })
public static boolean validate(@NotNull final Email email, @Nullable final EmailValidator emailValidator)
throws MailException {
LOGGER.debug("validating email...");

validateCompleteness(email);
validateAddresses(email, emailValidator);
scanForInjectionAttacks(email);

LOGGER.debug("...no problems found");

return true;
}

/**
* Checks whether:
* <ol>
* <li>there are recipients</li>
* <li>if there is a sender</li>
* <li>if there is a disposition notification TO if flag is set to use it</li>
* <li>if there is a return receipt TO if flag is set to use it</li>
* </ol>
*/
public static void validateCompleteness(final @NotNull Email email) {
// check for mandatory values
if (email.getRecipients().size() == 0) {
throw new MailValidationException(MailValidationException.MISSING_RECIPIENT);
throw new MailCompletenessException(MailCompletenessException.MISSING_RECIPIENT);
} else if (email.getFromRecipient() == null) {
throw new MailValidationException(MailValidationException.MISSING_SENDER);
throw new MailCompletenessException(MailCompletenessException.MISSING_SENDER);
} else if (email.isUseDispositionNotificationTo() && email.getDispositionNotificationTo() == null) {
throw new MailValidationException(MailValidationException.MISSING_DISPOSITIONNOTIFICATIONTO);
throw new MailCompletenessException(MailCompletenessException.MISSING_DISPOSITIONNOTIFICATIONTO);
} else if (email.isUseReturnReceiptTo() && email.getReturnReceiptTo() == null) {
throw new MailValidationException(MailValidationException.MISSING_RETURNRECEIPTTO);
} else
throw new MailCompletenessException(MailCompletenessException.MISSING_RETURNRECEIPTTO);
}
}

/**
* If email validator is provided, checks:
* <ol>
* <li>from recipient</li>
* <li>all TO/CC/BCC recipients</li>
* <li>reply-to recipient, if provided</li>
* <li>bounce-to recipient, if provided</li>
* <li>disposition-notification-to recipient, if provided</li>
* <li>return-receipt-to recipient, if provided</li>
* </ol>
*/
public static void validateAddresses(final @NotNull Email email, final @Nullable EmailValidator emailValidator) {
if (emailValidator != null) {
if (!emailValidator.isValid(email.getFromRecipient().getAddress())) {
throw new MailValidationException(format(MailValidationException.INVALID_SENDER, email));
throw new MailInvalidAddressException(format(MailInvalidAddressException.INVALID_SENDER, email));
}
for (final Recipient recipient : email.getRecipients()) {
if (!emailValidator.isValid(recipient.getAddress())) {
throw new MailValidationException(format(MailValidationException.INVALID_RECIPIENT, email));
throw new MailInvalidAddressException(format(MailInvalidAddressException.INVALID_RECIPIENT, email));
}
}
if (email.getReplyToRecipient() != null && !emailValidator.isValid(email.getReplyToRecipient().getAddress())) {
throw new MailValidationException(format(MailValidationException.INVALID_REPLYTO, email));
throw new MailInvalidAddressException(format(MailInvalidAddressException.INVALID_REPLYTO, email));
}
if (email.getBounceToRecipient() != null && !emailValidator.isValid(email.getBounceToRecipient().getAddress())) {
throw new MailValidationException(format(MailValidationException.INVALID_BOUNCETO, email));
throw new MailInvalidAddressException(format(MailInvalidAddressException.INVALID_BOUNCETO, email));
}
if (email.isUseDispositionNotificationTo()) {
if (!emailValidator.isValid(checkNonEmptyArgument(email.getDispositionNotificationTo(), "dispositionNotificationTo").getAddress())) {
throw new MailValidationException(format(MailValidationException.INVALID_DISPOSITIONNOTIFICATIONTO, email));
throw new MailInvalidAddressException(format(MailInvalidAddressException.INVALID_DISPOSITIONNOTIFICATIONTO, email));
}
}
if (email.isUseReturnReceiptTo()) {
if (!emailValidator.isValid(checkNonEmptyArgument(email.getReturnReceiptTo(), "returnReceiptTo").getAddress())) {
throw new MailValidationException(format(MailValidationException.INVALID_RETURNRECEIPTTO, email));
throw new MailInvalidAddressException(format(MailInvalidAddressException.INVALID_RETURNRECEIPTTO, email));
}
}
}
}

/**
* Checks the following headers for suspicious content (newlines and characters):
* <ol>
* <li>subject</li>
* <li>every header name and value</li>
* <li>every attachment name, nested datasource name and description</li>
* <li>every embedded image name, nested datasource name and description</li>
* <li>from recipient name and address</li>
* <li>replyTo recipient name and address, if provided</li>
* <li>bounceTo recipient name and address, if provided</li>
* <li>every TO/CC/BCC recipient name and address</li>
* <li>disposition-notification-to recipient name and address, if provided</li>
* <li>return-receipt-to recipient name and address, if provided</li>
* </ol>
*
* @see #scanForInjectionAttack
*/
public static void scanForInjectionAttacks(final @NotNull Email email) {
// check for illegal values
scanForInjectionAttack(email.getSubject(), "email.subject");
for (final Map.Entry<String, Collection<String>> headerEntry : email.getHeaders().entrySet()) {
for (final String headerValue : headerEntry.getValue()) {
// FIXME is this still needed?
scanForInjectionAttack(headerEntry.getKey(), "email.header.headerName");
scanForInjectionAttack(MimeUtility.unfold(headerValue), "email.header." + headerEntry.getKey());
scanForInjectionAttack(MimeUtility.unfold(headerValue), format("email.header.[%s]", headerEntry.getKey()));
}
}
for (final AttachmentResource attachment : email.getAttachments()) {
Expand Down Expand Up @@ -113,24 +172,20 @@ public static boolean validate(@NotNull final Email email, @Nullable final Email
scanForInjectionAttack(recipient.getName(), "email.recipient.name");
scanForInjectionAttack(recipient.getAddress(), "email.recipient.address");
}

LOGGER.debug("...no problems found");

return true;
}

/**
* @param value Value checked for suspicious newline characters "\n", "\r" and "%0A" (as acknowledged by SMTP servers).
* @param value Value checked for suspicious newline characters "\n", "\r" and the URL-encoded newline "%0A" (as acknowledged by SMTP servers).
* @param valueLabel The name of the field being checked, used for reporting exceptions.
*
* @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>
* @see <a href="https://security.stackexchange.com/a/54100/110048">StackExchange - What threats come from CRLF in email generation?</a>
* @see <a href="https://archive.ph/NuETu">OWASP - Testing for IMAP SMTP Injection</a>
* @see <a href="https://archive.ph/uReuD">CWE-93: Improper Neutralization of CRLF Sequences ('CRLF Injection')</a>
*/
private static void scanForInjectionAttack(final @Nullable String value, final String valueLabel) {
public static void scanForInjectionAttack(final @Nullable String value, final String valueLabel) {
if (value != null && (value.contains("\n") || value.contains("\r") || value.contains("%0A"))) {
throw new MailValidationException(format(MailValidationException.INJECTION_SUSPECTED, valueLabel, value));
throw new MailSuspiciousCRLFValueException(format(MailSuspiciousCRLFValueException.INJECTION_SUSPECTED, valueLabel, value));
}
}

Expand All @@ -152,4 +207,4 @@ public static MimeMessage signAndOrEncryptMessageWithSmime(@NotNull final Sessio
return ModuleLoader.loadSmimeModule()
.signAndOrEncryptEmail(session, messageToProtect, emailContainingSmimeDetails, defaultSmimeSigningStore);
}
}
}

0 comments on commit 09f74df

Please sign in to comment.