From 64b6c98beb2740dcb55625c5ac0883f81a6530fb Mon Sep 17 00:00:00 2001 From: lavinhoque33 Date: Sat, 3 Oct 2026 22:38:00 -0500 Subject: [PATCH 1/2] Encode credentials in Docker Compose MongoDB connection string MongoDockerComposeConnectionDetailsFactory built the connection string by appending MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_ROOT_PASSWORD without encoding them. The mongo image accepts any password, but a value containing a reserved character such as '@', ':' or '/' produced an invalid connection string and the application failed to start. Percent-encode the username and password, as PropertiesMongoConnectionDetails already does. Spaces are encoded as %20 because the MongoDB driver does not decode '+' to a space. See gh-51939 Signed-off-by: lavinhoque33 --- ...nectionDetailsFactoryIntegrationTests.java | 14 +++ ...ved-characters-in-credentials-compose.yaml | 9 ++ ...DockerComposeConnectionDetailsFactory.java | 11 ++- ...nDetailsFactoryConnectionDetailsTests.java | 93 +++++++++++++++++++ 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 module/spring-boot-mongodb/src/dockerTest/resources/org/springframework/boot/mongodb/docker/compose/mongo-with-reserved-characters-in-credentials-compose.yaml create mode 100644 module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java diff --git a/module/spring-boot-mongodb/src/dockerTest/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-mongodb/src/dockerTest/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryIntegrationTests.java index 91b70cac78f..cd1e5e20f62 100644 --- a/module/spring-boot-mongodb/src/dockerTest/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-mongodb/src/dockerTest/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -18,6 +18,8 @@ package org.springframework.boot.mongodb.docker.compose; import com.mongodb.ConnectionString; import com.mongodb.MongoCredential; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; import org.springframework.boot.mongodb.autoconfigure.MongoConnectionDetails; @@ -50,6 +52,18 @@ class MongoDockerComposeConnectionDetailsFactoryIntegrationTests { assertThat(sslBundle).isNotNull(); } + @DockerComposeTest(composeFile = "mongo-with-reserved-characters-in-credentials-compose.yaml", + image = TestImage.MONGODB) + void runWithReservedCharactersInCredentialsCreatesConnectionDetails(MongoConnectionDetails connectionDetails) { + ConnectionString connectionString = connectionDetails.getConnectionString(); + MongoCredential credential = connectionString.getCredential(); + assertThat(credential).isNotNull(); + assertThat(credential.getPassword()).isEqualTo("p@ss:w/rd".toCharArray()); + try (MongoClient client = MongoClients.create(connectionString)) { + assertThat(client.listDatabaseNames()).contains("admin"); + } + } + private void assertConnectionDetailsWithDatabase(MongoConnectionDetails connectionDetails, String database) { ConnectionString connectionString = connectionDetails.getConnectionString(); MongoCredential credential = connectionString.getCredential(); diff --git a/module/spring-boot-mongodb/src/dockerTest/resources/org/springframework/boot/mongodb/docker/compose/mongo-with-reserved-characters-in-credentials-compose.yaml b/module/spring-boot-mongodb/src/dockerTest/resources/org/springframework/boot/mongodb/docker/compose/mongo-with-reserved-characters-in-credentials-compose.yaml new file mode 100644 index 00000000000..1ea6161603f --- /dev/null +++ b/module/spring-boot-mongodb/src/dockerTest/resources/org/springframework/boot/mongodb/docker/compose/mongo-with-reserved-characters-in-credentials-compose.yaml @@ -0,0 +1,9 @@ +services: + mongo: + image: '{imageName}' + ports: + - '27017' + environment: + MONGO_INITDB_ROOT_USERNAME: 'root' + MONGO_INITDB_ROOT_PASSWORD: 'p@ss:w/rd' + MONGO_INITDB_DATABASE: 'mydatabase' diff --git a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactory.java b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactory.java index 407f45c8911..691ab24c3eb 100644 --- a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactory.java @@ -16,6 +16,9 @@ package org.springframework.boot.mongodb.docker.compose; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + import com.mongodb.ConnectionString; import org.jspecify.annotations.Nullable; @@ -69,9 +72,9 @@ class MongoDockerComposeConnectionDetailsFactory extends DockerComposeConnection MongoEnvironment environment = new MongoEnvironment(service.env()); StringBuilder builder = new StringBuilder("mongodb://"); if (environment.getUsername() != null) { - builder.append(environment.getUsername()); + builder.append(encode(environment.getUsername())); builder.append(":"); - builder.append((environment.getPassword() != null) ? environment.getPassword() : ""); + builder.append((environment.getPassword() != null) ? encode(environment.getPassword()) : ""); builder.append("@"); } builder.append(service.host()); @@ -85,6 +88,10 @@ class MongoDockerComposeConnectionDetailsFactory extends DockerComposeConnection return new ConnectionString(builder.toString()); } + private String encode(String input) { + return URLEncoder.encode(input, StandardCharsets.UTF_8).replace("+", "%20"); + } + @Override public @Nullable SslBundle getSslBundle() { return this.sslBundle; diff --git a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java new file mode 100644 index 00000000000..a935bcbf19b --- /dev/null +++ b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.mongodb.docker.compose; + +import java.util.Collections; +import java.util.Map; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoCredential; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.ConnectionPorts; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.mongodb.docker.compose.MongoDockerComposeConnectionDetailsFactory.MongoDockerComposeConnectionDetails; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for + * {@link MongoDockerComposeConnectionDetailsFactory.MongoDockerComposeConnectionDetails}. + * + * @author Iram Tazim Hoque + */ +class MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests { + + private final RunningService service = mock(RunningService.class); + + MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests() { + given(this.service.labels()).willReturn(Collections.emptyMap()); + ConnectionPorts connectionPorts = mock(ConnectionPorts.class); + given(this.service.ports()).willReturn(connectionPorts); + given(this.service.host()).willReturn("localhost"); + given(connectionPorts.get(27017)).willReturn(30001); + } + + @Test + void createConnectionDetails() { + given(this.service.env()).willReturn(Map.of("MONGO_INITDB_ROOT_USERNAME", "root", "MONGO_INITDB_ROOT_PASSWORD", + "secret", "MONGO_INITDB_DATABASE", "mydatabase")); + ConnectionString connectionString = getConnectionString(); + assertThat(connectionString.getHosts()).containsExactly("localhost:30001"); + assertThat(connectionString.getDatabase()).isEqualTo("mydatabase"); + MongoCredential credential = connectionString.getCredential(); + assertThat(credential).isNotNull(); + assertThat(credential.getUserName()).isEqualTo("root"); + assertThat(credential.getPassword()).isEqualTo("secret".toCharArray()); + assertThat(credential.getSource()).isEqualTo("admin"); + } + + @Test + void createConnectionDetailsWhenCredentialsContainReservedCharacters() { + given(this.service.env()).willReturn(Map.of("MONGO_INITDB_ROOT_USERNAME", "us@r:name", + "MONGO_INITDB_ROOT_PASSWORD", "p@ss:w/rd%?#[] +", "MONGO_INITDB_DATABASE", "mydatabase")); + ConnectionString connectionString = getConnectionString(); + assertThat(connectionString.getHosts()).containsExactly("localhost:30001"); + assertThat(connectionString.getDatabase()).isEqualTo("mydatabase"); + MongoCredential credential = connectionString.getCredential(); + assertThat(credential).isNotNull(); + assertThat(credential.getUserName()).isEqualTo("us@r:name"); + assertThat(credential.getPassword()).isEqualTo("p@ss:w/rd%?#[] +".toCharArray()); + assertThat(credential.getSource()).isEqualTo("admin"); + } + + @Test + void createConnectionDetailsWithoutCredentials() { + given(this.service.env()).willReturn(Collections.emptyMap()); + ConnectionString connectionString = getConnectionString(); + assertThat(connectionString.getHosts()).containsExactly("localhost:30001"); + assertThat(connectionString.getDatabase()).isEqualTo("test"); + assertThat(connectionString.getCredential()).isNull(); + } + + private ConnectionString getConnectionString() { + return new MongoDockerComposeConnectionDetails(this.service).getConnectionString(); + } + +} From eb0887085e487198896ccd1e5ecf282e51b95006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 7 Oct 2026 15:30:45 +0200 Subject: [PATCH 2/2] Polish "Encode credentials in Docker Compose MongoDB connection string" See gh-51939 --- ...nectionDetailsFactoryIntegrationTests.java | 14 ------ ...ved-characters-in-credentials-compose.yaml | 9 ---- ...DockerComposeConnectionDetailsFactory.java | 2 +- ...ComposeConnectionDetailsFactoryTests.java} | 45 ++++++++++--------- 4 files changed, 24 insertions(+), 46 deletions(-) delete mode 100644 module/spring-boot-mongodb/src/dockerTest/resources/org/springframework/boot/mongodb/docker/compose/mongo-with-reserved-characters-in-credentials-compose.yaml rename module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/docker/compose/{MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java => MongoDockerComposeConnectionDetailsFactoryTests.java} (68%) diff --git a/module/spring-boot-mongodb/src/dockerTest/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-mongodb/src/dockerTest/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryIntegrationTests.java index cd1e5e20f62..91b70cac78f 100644 --- a/module/spring-boot-mongodb/src/dockerTest/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-mongodb/src/dockerTest/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -18,8 +18,6 @@ package org.springframework.boot.mongodb.docker.compose; import com.mongodb.ConnectionString; import com.mongodb.MongoCredential; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; import org.springframework.boot.mongodb.autoconfigure.MongoConnectionDetails; @@ -52,18 +50,6 @@ class MongoDockerComposeConnectionDetailsFactoryIntegrationTests { assertThat(sslBundle).isNotNull(); } - @DockerComposeTest(composeFile = "mongo-with-reserved-characters-in-credentials-compose.yaml", - image = TestImage.MONGODB) - void runWithReservedCharactersInCredentialsCreatesConnectionDetails(MongoConnectionDetails connectionDetails) { - ConnectionString connectionString = connectionDetails.getConnectionString(); - MongoCredential credential = connectionString.getCredential(); - assertThat(credential).isNotNull(); - assertThat(credential.getPassword()).isEqualTo("p@ss:w/rd".toCharArray()); - try (MongoClient client = MongoClients.create(connectionString)) { - assertThat(client.listDatabaseNames()).contains("admin"); - } - } - private void assertConnectionDetailsWithDatabase(MongoConnectionDetails connectionDetails, String database) { ConnectionString connectionString = connectionDetails.getConnectionString(); MongoCredential credential = connectionString.getCredential(); diff --git a/module/spring-boot-mongodb/src/dockerTest/resources/org/springframework/boot/mongodb/docker/compose/mongo-with-reserved-characters-in-credentials-compose.yaml b/module/spring-boot-mongodb/src/dockerTest/resources/org/springframework/boot/mongodb/docker/compose/mongo-with-reserved-characters-in-credentials-compose.yaml deleted file mode 100644 index 1ea6161603f..00000000000 --- a/module/spring-boot-mongodb/src/dockerTest/resources/org/springframework/boot/mongodb/docker/compose/mongo-with-reserved-characters-in-credentials-compose.yaml +++ /dev/null @@ -1,9 +0,0 @@ -services: - mongo: - image: '{imageName}' - ports: - - '27017' - environment: - MONGO_INITDB_ROOT_USERNAME: 'root' - MONGO_INITDB_ROOT_PASSWORD: 'p@ss:w/rd' - MONGO_INITDB_DATABASE: 'mydatabase' diff --git a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactory.java b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactory.java index 691ab24c3eb..fe00b70a3c9 100644 --- a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactory.java @@ -88,7 +88,7 @@ class MongoDockerComposeConnectionDetailsFactory extends DockerComposeConnection return new ConnectionString(builder.toString()); } - private String encode(String input) { + private static String encode(String input) { return URLEncoder.encode(input, StandardCharsets.UTF_8).replace("+", "%20"); } diff --git a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryTests.java similarity index 68% rename from module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java rename to module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryTests.java index a935bcbf19b..5f952fde18f 100644 --- a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java +++ b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/docker/compose/MongoDockerComposeConnectionDetailsFactoryTests.java @@ -32,28 +32,19 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** - * Tests for - * {@link MongoDockerComposeConnectionDetailsFactory.MongoDockerComposeConnectionDetails}. + * Tests for {@link MongoDockerComposeConnectionDetailsFactory}. * * @author Iram Tazim Hoque */ -class MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests { - - private final RunningService service = mock(RunningService.class); - - MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests() { - given(this.service.labels()).willReturn(Collections.emptyMap()); - ConnectionPorts connectionPorts = mock(ConnectionPorts.class); - given(this.service.ports()).willReturn(connectionPorts); - given(this.service.host()).willReturn("localhost"); - given(connectionPorts.get(27017)).willReturn(30001); - } +class MongoDockerComposeConnectionDetailsFactoryTests { @Test void createConnectionDetails() { - given(this.service.env()).willReturn(Map.of("MONGO_INITDB_ROOT_USERNAME", "root", "MONGO_INITDB_ROOT_PASSWORD", - "secret", "MONGO_INITDB_DATABASE", "mydatabase")); - ConnectionString connectionString = getConnectionString(); + RunningService runningService = mockRunningService(); + given(runningService.env()).willReturn(Map.of("MONGO_INITDB_ROOT_USERNAME", "root", + "MONGO_INITDB_ROOT_PASSWORD", "secret", "MONGO_INITDB_DATABASE", "mydatabase")); + ConnectionString connectionString = new MongoDockerComposeConnectionDetails(runningService) + .getConnectionString(); assertThat(connectionString.getHosts()).containsExactly("localhost:30001"); assertThat(connectionString.getDatabase()).isEqualTo("mydatabase"); MongoCredential credential = connectionString.getCredential(); @@ -65,9 +56,11 @@ class MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests { @Test void createConnectionDetailsWhenCredentialsContainReservedCharacters() { - given(this.service.env()).willReturn(Map.of("MONGO_INITDB_ROOT_USERNAME", "us@r:name", + RunningService runningService = mockRunningService(); + given(runningService.env()).willReturn(Map.of("MONGO_INITDB_ROOT_USERNAME", "us@r:name", "MONGO_INITDB_ROOT_PASSWORD", "p@ss:w/rd%?#[] +", "MONGO_INITDB_DATABASE", "mydatabase")); - ConnectionString connectionString = getConnectionString(); + ConnectionString connectionString = new MongoDockerComposeConnectionDetails(runningService) + .getConnectionString(); assertThat(connectionString.getHosts()).containsExactly("localhost:30001"); assertThat(connectionString.getDatabase()).isEqualTo("mydatabase"); MongoCredential credential = connectionString.getCredential(); @@ -79,15 +72,23 @@ class MongoDockerComposeConnectionDetailsFactoryConnectionDetailsTests { @Test void createConnectionDetailsWithoutCredentials() { - given(this.service.env()).willReturn(Collections.emptyMap()); - ConnectionString connectionString = getConnectionString(); + RunningService runningService = mockRunningService(); + given(runningService.env()).willReturn(Collections.emptyMap()); + ConnectionString connectionString = new MongoDockerComposeConnectionDetails(runningService) + .getConnectionString(); assertThat(connectionString.getHosts()).containsExactly("localhost:30001"); assertThat(connectionString.getDatabase()).isEqualTo("test"); assertThat(connectionString.getCredential()).isNull(); } - private ConnectionString getConnectionString() { - return new MongoDockerComposeConnectionDetails(this.service).getConnectionString(); + private static RunningService mockRunningService() { + RunningService service = mock(RunningService.class); + given(service.labels()).willReturn(Collections.emptyMap()); + ConnectionPorts connectionPorts = mock(ConnectionPorts.class); + given(service.ports()).willReturn(connectionPorts); + given(service.host()).willReturn("localhost"); + given(connectionPorts.get(27017)).willReturn(30001); + return service; } }