You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2022/09/23 01:45:06 UTC
[james-project] 02/05: JAMES-3431 Stricter validation for DSN ENVID (#1002)
This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch 3.7.x
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 3f5e7795162112e203b918ea6ef46d6d61833451
Author: Benoit TELLIER <bt...@linagora.com>
AuthorDate: Tue May 24 08:55:35 2022 +0700
JAMES-3431 Stricter validation for DSN ENVID (#1002)
Setting an invalid EndId leads to emails not being
transmissible to third party systems. Stricter validation
fixes this.
We need to match the XText syntax. Provides a utility to
check, encode and decode XText.
See https://datatracker.ietf.org/doc/html/rfc3461#section-4
(cherry picked from commit 5ad5cb6a4e07432502350f4ee1dee9a50fae9d01)
---
.../main/java/org/apache/mailet/DsnParameters.java | 93 ++++++++++++++++++++++
.../java/org/apache/mailet/DsnParametersTest.java | 68 +++++++++++++---
.../transport/matchers/DSNDelayRequestedTest.java | 2 -
3 files changed, 152 insertions(+), 11 deletions(-)
diff --git a/mailet/api/src/main/java/org/apache/mailet/DsnParameters.java b/mailet/api/src/main/java/org/apache/mailet/DsnParameters.java
index 5d23938d6e..a426dceb01 100644
--- a/mailet/api/src/main/java/org/apache/mailet/DsnParameters.java
+++ b/mailet/api/src/main/java/org/apache/mailet/DsnParameters.java
@@ -32,6 +32,7 @@ import org.apache.commons.lang3.tuple.Pair;
import org.apache.james.core.MailAddress;
import com.github.fge.lambdas.Throwing;
+import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
@@ -52,6 +53,96 @@ public class DsnParameters {
public static final String ENVID_PARAMETER = "ENVID";
public static final String RET_PARAMETER = "RET";
+ public static class XText {
+ private static final CharMatcher ESCAPE_CHAR = CharMatcher.isNot('+');
+ // https://datatracker.ietf.org/doc/html/rfc3461#section-4
+ private static CharMatcher XCHAR_VALIDATOR = CharMatcher.inRange('!', '~')
+ .and(CharMatcher.isNot('='))
+ .and(ESCAPE_CHAR);
+ private static CharMatcher HEX_VALIDATOR = CharMatcher.inRange('0', '9')
+ .or(CharMatcher.inRange('A', 'F'));
+
+ static String encode(String text) {
+ if (isOnlyXChar(text)) {
+ return text;
+ }
+ StringBuilder result = new StringBuilder();
+ text.chars()
+ .forEach(i -> encode(i, result));
+ return result.toString();
+ }
+
+ private static StringBuilder encode(int i, StringBuilder result) {
+ if (i < 33 || i > 126 || i == 43 || i == 61) {
+ return result.append('+')
+ .append(String.format("%02X", i));
+ }
+ return result.append((char) i);
+ }
+
+ static String decode(String text) {
+ Preconditions.checkArgument(isValid(text), "Decoding invalid xtext '%s'", text);
+ if (XCHAR_VALIDATOR.matchesAllOf(text)) {
+ return text;
+ }
+ char[] chars = text.toCharArray();
+ StringBuilder result = new StringBuilder();
+ int i = 0;
+ while (i < chars.length) {
+ char c = chars[i];
+ if (c == 43) {
+ // + escape sequence
+ char hex1 = chars[i + 1];
+ char hex2 = chars[i + 2];
+ char hexChar = (char) Integer.parseInt("" + hex1 + hex2, 16);
+ result.append(hexChar);
+ i += 3;
+ } else {
+ result.append(c);
+ i++;
+ }
+ }
+ return result.toString();
+ }
+
+ static boolean isValid(String text) {
+ if (isOnlyXChar(text)) {
+ return true;
+ }
+ char[] chars = text.toCharArray();
+ int i = 0;
+ while (i < chars.length) {
+ char c = chars[i];
+ if (!XCHAR_VALIDATOR.matches(c)) {
+ if (c == 43) {
+ // + escape sequence
+ if (!isValidEscapeSequence(chars, i)) {
+ return false;
+ }
+ i += 3;
+ } else {
+ return false;
+ }
+ } else {
+ i++;
+ }
+ }
+ return true;
+ }
+
+ private static boolean isValidEscapeSequence(char[] chars, int i) {
+ if (i + 3 > chars.length) {
+ return false;
+ }
+ return HEX_VALIDATOR.matches(chars[i + 1])
+ && HEX_VALIDATOR.matches(chars[i + 2]);
+ }
+
+ private static boolean isOnlyXChar(String text) {
+ return XCHAR_VALIDATOR.matchesAllOf(text);
+ }
+ }
+
/**
* RET parameter allow the sender to control which part of the bounced message should be returned to the sender.
*
@@ -104,6 +195,8 @@ public class DsnParameters {
public static EnvId of(String value) {
Preconditions.checkNotNull(value);
+ Preconditions.checkArgument(XText.isValid(value), "According to RFC-3461 EnvId should be a valid xtext" +
+ ", thus composed of CHARs between \"!\" (33) and \"~\" (126) inclusive, except for \"+\" and \"=\" or follow the hexadecimal escape sequence.");
return new EnvId(value);
}
diff --git a/mailet/api/src/test/java/org/apache/mailet/DsnParametersTest.java b/mailet/api/src/test/java/org/apache/mailet/DsnParametersTest.java
index ff0e6c3ee6..ba0c0c8b93 100644
--- a/mailet/api/src/test/java/org/apache/mailet/DsnParametersTest.java
+++ b/mailet/api/src/test/java/org/apache/mailet/DsnParametersTest.java
@@ -29,8 +29,11 @@ import org.apache.mailet.DsnParameters.EnvId;
import org.apache.mailet.DsnParameters.Notify;
import org.apache.mailet.DsnParameters.RecipientDsnParameters;
import org.apache.mailet.DsnParameters.Ret;
+import org.apache.mailet.DsnParameters.XText;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import com.google.common.collect.ImmutableMap;
@@ -113,19 +116,23 @@ class DsnParametersTest {
@Nested
class EnvIdTest {
@Test
- void parseShouldReturnEmptyOnUnknownValue() {
- assertThat(Ret.parse("unknown")).isEmpty();
+ void ofShouldThrowOnNullValue() {
+ assertThatThrownBy(() -> EnvId.of(null))
+ .isInstanceOf(NullPointerException.class);
}
- @Test
- void parseShouldReturnEmptyOnEmptyValue() {
- assertThat(EnvId.of("").asString()).isEqualTo("");
+ @ParameterizedTest
+ @ValueSource(strings = {"bad envid", "bad=envid", "Bad+envId", "Bad+"})
+ void ofShouldThrowOnBadValue(String badValue) {
+ assertThatThrownBy(() -> EnvId.of(badValue))
+ .isInstanceOf(IllegalArgumentException.class);
}
- @Test
- void ofShouldThrowOnNullValue() {
- assertThatThrownBy(() -> EnvId.of(null))
- .isInstanceOf(NullPointerException.class);
+ @ParameterizedTest
+ @ValueSource(strings = {"good", "000023", "good+32space", "+32", "", "+61", "+43"})
+ void ofShouldAcceptGoodValue(String value) {
+ assertThat(EnvId.of(value).asString())
+ .isEqualTo(value);
}
@Test
@@ -147,6 +154,49 @@ class DsnParametersTest {
}
}
+ @Nested
+ class XTextTest {
+ @ParameterizedTest
+ @ValueSource(strings = {"bad envid", "bad=envid", "Bad+envId", "Bad+", "Bad+2"})
+ void isValidShouldReturnFalseOnBadValue(String badValue) {
+ assertThat(XText.isValid(badValue))
+ .isFalse();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"bad envid", "bad=envid", "Bad+envId", "Bad+", "Bad+0o", "Bad+2"})
+ void decodeShouldThrowWhenInvalid(String badValue) {
+ assertThatThrownBy(() -> XText.decode(badValue))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"good", "000023", "", "good+32space", "+32", "+61", "+43"})
+ void isValidShouldReturnTrueOnGoodValue(String value) {
+ assertThat(XText.isValid(value))
+ .isTrue();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"abc", "a+32bc", "", " &*^%$@#%^&#%%kefbvvule89623r757'\"\\/?<>=+-_", "0123"})
+ void encodeDecodeShouldNoop(String value) {
+ assertThat(XText.decode(XText.encode(value)))
+ .isEqualTo(value);
+ }
+
+ @Test
+ void encodeShouldReturnTheGoodValue() {
+ assertThat(XText.encode("abc def+0123=gdef"))
+ .isEqualTo("abc+20def+2B0123+3Dgdef");
+ }
+
+ @Test
+ void decodeShouldReturnTheGoodValue() {
+ assertThat(XText.decode("abc+20def+2B0123+3Dgdef"))
+ .isEqualTo("abc def+0123=gdef");
+ }
+ }
+
@Nested
class NotifyTest {
@Test
diff --git a/server/mailet/mailets/src/test/java/org/apache/james/transport/matchers/DSNDelayRequestedTest.java b/server/mailet/mailets/src/test/java/org/apache/james/transport/matchers/DSNDelayRequestedTest.java
index 7a6f714729..62fe867e18 100644
--- a/server/mailet/mailets/src/test/java/org/apache/james/transport/matchers/DSNDelayRequestedTest.java
+++ b/server/mailet/mailets/src/test/java/org/apache/james/transport/matchers/DSNDelayRequestedTest.java
@@ -27,13 +27,11 @@ import static org.apache.mailet.base.MailAddressFixture.RECIPIENT1;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.junit.jupiter.api.Assertions.*;
import java.util.EnumSet;
import org.apache.james.server.core.MailImpl;
import org.apache.mailet.DsnParameters;
-import org.apache.mailet.base.MailAddressFixture;
import org.apache.mailet.base.test.FakeMatcherConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org