This is an automated email from the ASF dual-hosted git repository. quantranhong1999 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit e79eec96071165e5700e3c630f41376cea57d480 Author: Quan Tran <[email protected]> AuthorDate: Thu Aug 13 09:46:32 2026 +0700 JAMES-4210 Harden ManageSieve STARTTLS capability handling Reject STARTTLS when transport encryption is unavailable, avoid advertising unusable STARTTLS or empty SASL capabilities, and ensure malformed rejected-STARTTLS sequences close instead of hanging the connection. --- .../james/managesieve/core/CoreProcessor.java | 3 + .../james/managesieveserver/CapabilityTest.java | 73 ++++++++++++++++++++-- .../james/managesieveserver/ManageSieveClient.java | 4 ++ 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java index 4b6186e910..c91f955c90 100644 --- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java +++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java @@ -227,6 +227,9 @@ public class CoreProcessor implements CoreCommands { @Override public String startTLS(Session session) { + if (!session.supportStartTLS()) { + return "NO STARTTLS is not available"; + } if (session.getState() == Session.State.UNAUTHENTICATED) { if (session.isSslEnabled()) { return "NO You can't enable two time SSL encryption"; diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/CapabilityTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/CapabilityTest.java index f13ffe3d8f..5ea4003657 100644 --- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/CapabilityTest.java +++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/CapabilityTest.java @@ -19,7 +19,14 @@ package org.apache.james.managesieveserver; -import org.assertj.core.api.Assertions; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.EOFException; + +import org.apache.commons.configuration2.HierarchicalConfiguration; +import org.apache.commons.configuration2.tree.ImmutableNode; +import org.apache.james.server.core.configuration.FileConfigurationProvider; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -42,11 +49,11 @@ class CapabilityTest { ManageSieveClient client = new ManageSieveClient(); client.connect(this.testSystem.getBindedIP(), this.testSystem.getBindedPort()); ManageSieveClient.ServerResponse initialGreeting = client.readResponse(); - Assertions.assertThat(getSASLMechanisms(initialGreeting)).containsExactlyInAnyOrder("PLAIN"); + assertThat(getSASLMechanisms(initialGreeting)).containsExactlyInAnyOrder("PLAIN"); client.sendCommand("CAPABILITY"); ManageSieveClient.ServerResponse capabilityResponse = client.readResponse(); - Assertions.assertThat(getSASLMechanisms(capabilityResponse)).containsExactlyInAnyOrder("PLAIN"); + assertThat(getSASLMechanisms(capabilityResponse)).containsExactlyInAnyOrder("PLAIN"); } @Test @@ -56,15 +63,69 @@ class CapabilityTest { ManageSieveClient client = new ManageSieveClient(); client.connect(this.testSystem.getBindedIP(), this.testSystem.getBindedPort()); ManageSieveClient.ServerResponse initialGreeting = client.readResponse(); - Assertions.assertThat(getSASLMechanisms(initialGreeting)).containsExactlyInAnyOrder("PLAIN", "XOAUTH2", "OAUTHBEARER"); + assertThat(getSASLMechanisms(initialGreeting)).containsExactlyInAnyOrder("PLAIN", "XOAUTH2", "OAUTHBEARER"); client.sendCommand("CAPABILITY"); ManageSieveClient.ServerResponse capabilityResponse = client.readResponse(); - Assertions.assertThat(getSASLMechanisms(capabilityResponse)).containsExactlyInAnyOrder("PLAIN", "XOAUTH2", "OAUTHBEARER"); + assertThat(getSASLMechanisms(capabilityResponse)).containsExactlyInAnyOrder("PLAIN", "XOAUTH2", "OAUTHBEARER"); + } + + @Test + void shouldNotAnnounceOrAuthPlainOnClearTextWhenSslIsRequired() throws Exception { + HierarchicalConfiguration<ImmutableNode> configuration = FileConfigurationProvider.getConfig( + ClassLoader.getSystemResourceAsStream("managesieveserver.xml")); + configuration.addProperty("auth.requireSSL", true); + this.testSystem.setUp(configuration); + + ManageSieveClient client = new ManageSieveClient(); + client.connect(this.testSystem.getBindedIP(), this.testSystem.getBindedPort()); + client.readResponse(); + + client.sendCommand("CAPABILITY"); + ManageSieveClient.ServerResponse capabilityResponse = client.readResponse(); + // RFC 5804 section 1.7 only permits an empty SASL capability when STARTTLS is advertised (which is not the case here) + assertThat(capabilityResponse.responseLines()).noneMatch(line -> line.startsWith("\"SASL\"")); + + client.sendCommand("AUTHENTICATE \"PLAIN\""); + ManageSieveClient.ServerResponse authenticationResponse = client.readResponse(); + assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO); + assertThat(authenticationResponse.responseCode()).contains("ENCRYPT-NEEDED"); + } + + @Test + void shouldNotAnnounceOrAcceptStartTlsWhenTlsIsUnavailable() throws Exception { + this.testSystem.setUp(); + + ManageSieveClient client = new ManageSieveClient(); + client.connect(this.testSystem.getBindedIP(), this.testSystem.getBindedPort()); + ManageSieveClient.ServerResponse initialGreeting = client.readResponse(); + assertThat(initialGreeting.responseLines()).doesNotContain("\"STARTTLS\""); + + client.sendCommand("CAPABILITY"); + ManageSieveClient.ServerResponse capabilityResponse = client.readResponse(); + assertThat(capabilityResponse.responseLines()).doesNotContain("\"STARTTLS\""); + + client.sendCommand("STARTTLS"); + ManageSieveClient.ServerResponse startTlsResponse = client.readResponse(); + assertThat(startTlsResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO); + assertThat(startTlsResponse.explanation()).contains("STARTTLS is not available"); + } + + @Test + void shouldNotHangConnectionWhenAnotherCommandFollowsRejectedStartTls() throws Exception { + this.testSystem.setUp(); + + ManageSieveClient client = new ManageSieveClient(); + client.connect(this.testSystem.getBindedIP(), this.testSystem.getBindedPort()); + client.readResponse(); + + client.sendCommand("STARTTLS\r\nNOOP"); + + assertThatThrownBy(client::readResponse).isInstanceOf(EOFException.class); } private String[] getSASLMechanisms(ManageSieveClient.ServerResponse response) { - String saslLine = Assertions.assertThat(response.responseLines()) + String saslLine = assertThat(response.responseLines()) .filteredOn(line -> line.startsWith("\"SASL\"")) .hasSize(1) .first() diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java index 4a2a101b4d..104a546b28 100644 --- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java +++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java @@ -21,6 +21,7 @@ package org.apache.james.managesieveserver; import java.io.BufferedReader; import java.io.BufferedWriter; +import java.io.EOFException; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; @@ -72,6 +73,9 @@ public class ManageSieveClient extends SocketClient { ArrayList<String> lines = new ArrayList<>(); while (response == null) { String line = this.reader.readLine(); + if (line == null) { + throw new EOFException("ManageSieve connection closed without a response"); + } String[] tokens = line.split(" ", 3); if (EnumUtils.isValidEnumIgnoreCase(ResponseType.class, tokens[0])) { ResponseType responseType = EnumUtils.getEnumIgnoreCase(ResponseType.class, tokens[0]); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
