Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,4 @@ https://github.com/mongodb/mongo-java-driver.
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.

45 changes: 41 additions & 4 deletions driver-core/src/main/com/mongodb/ConnectionString.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@
import java.util.stream.Stream;

import static com.mongodb.MongoCredential.ALLOWED_HOSTS_KEY;
import static com.mongodb.internal.connection.DomainNameUtils.normalizeSrvAllowedHostsSuffix;
import static com.mongodb.internal.connection.OidcAuthenticator.OidcValidator.validateCreateOidcCredential;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;


/**
* <p>Represents a <a href="https://www.mongodb.com/docs/manual/reference/connection-string/">Connection String</a>.
* The Connection String describes the hosts to be used and options.</p>
Expand Down Expand Up @@ -266,6 +266,13 @@
* <ul>
* <li>{@code srvServiceName=string}: The SRV service name. See {@link ClusterSettings#getSrvServiceName()} for details.</li>
* <li>{@code srvMaxHosts=n}: The maximum number of hosts from the SRV record to connect to.</li>
* <li>{@code srvAllowedHostsSuffix=string}: The hostname suffix used to validate hosts returned via SRV lookup, replacing the domain

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec requires a clearly visible warning:

Notably, srvAllowedHostsSuffix relaxes existing security measures and must be used with caution. Thus, drivers MUST document that this parameter is a dangerous option. For example, something like "WARNING: Modifying the default SRV domain name validation can create vulnerabilities." should be clearly visible in the documentation.

Suggest adding the above plus a sentence that multi-label public suffixes are not rejected see C# and Python.

Comment thread
stIncMale marked this conversation as resolved.
* inferred from the SRV host name. Only valid with the mongodb+srv protocol. <b>WARNING:</b> Modifying the default SRV domain name
* validation can create vulnerabilities. This option relaxes a built-in DNS spoofing safeguard. Use the most specific suffix possible for
* your deployment rather than a broad company-wide domain. For example, instead of {@code "mongodb+srv://cluster.test.internal.example
* .com/?srvAllowedHostsSuffix=.example.com"} which would accept any host across the entire domain, scope it further like so:
* {@code "mongodb+srv://cluster.test.internal.example.com/?srvAllowedHostsSuffix=.internal.example.com"}.
* </li>
* </ul>
* <p>General configuration:</p>
* <ul>
Expand Down Expand Up @@ -309,6 +316,7 @@ public class ConnectionString {

private Integer srvMaxHosts;
private String srvServiceName;
private String srvAllowedHostsSuffix;
private Boolean directConnection;
private Boolean loadBalanced;
private ReadPreference readPreference;
Expand Down Expand Up @@ -488,6 +496,14 @@ public ConnectionString(final String connectionString, @Nullable final DnsClient
throw new IllegalArgumentException("srvServiceName can only be specified with mongodb+srv protocol");
}

if (!isSrvProtocol && srvAllowedHostsSuffix != null) {
throw new IllegalArgumentException("srvAllowedHostsSuffix can only be specified with mongodb+srv protocol");
}

if (srvAllowedHostsSuffix != null) {
srvAllowedHostsSuffix = normalizeSrvAllowedHostsSuffix(srvAllowedHostsSuffix);
}

if (directConnection != null && directConnection) {
if (isSrvProtocol) {
throw new IllegalArgumentException("Direct connections are not supported when using mongodb+srv protocol");
Expand Down Expand Up @@ -579,6 +595,7 @@ public ConnectionString(final String connectionString, @Nullable final DnsClient

GENERAL_OPTIONS_KEYS.add("srvmaxhosts");
GENERAL_OPTIONS_KEYS.add("srvservicename");
GENERAL_OPTIONS_KEYS.add("srvallowedhostssuffix");

COMPRESSOR_KEYS.add("compressors");
COMPRESSOR_KEYS.add("zlibcompressionlevel");
Expand Down Expand Up @@ -744,6 +761,9 @@ private void translateOptions(final Map<String, List<String>> optionsMap) {
case "srvservicename":
srvServiceName = value;
break;
case "srvallowedhostssuffix":
srvAllowedHostsSuffix = value;
break;
default:
break;
}
Expand Down Expand Up @@ -1375,6 +1395,22 @@ public String getSrvServiceName() {
return srvServiceName;
}

/**
* Gets the SRV allowed hosts suffix.
*
* <p>If present, its value is used as the domain for SRV host name validation, replacing the domain inferred from
* the SRV host name. The value is normalized: a leading {@code "."} is prepended if absent, so the returned value
* always begins with {@code "."}.</p>
*
* @return the normalized SRV allowed hosts suffix, always beginning with {@code "."}. Defaults to null.
* @since 5.13
* @see ClusterSettings#getSrvAllowedHostsSuffix()
*/
@Nullable
public String getSrvAllowedHostsSuffix() {
return srvAllowedHostsSuffix;
}

/**
* Gets the list of hosts
*
Expand Down Expand Up @@ -1875,7 +1911,8 @@ public boolean equals(final Object o) {
&& Objects.equals(compressorList, that.compressorList)
&& Objects.equals(uuidRepresentation, that.uuidRepresentation)
&& Objects.equals(srvServiceName, that.srvServiceName)
&& Objects.equals(srvMaxHosts, that.srvMaxHosts);
&& Objects.equals(srvMaxHosts, that.srvMaxHosts)
&& Objects.equals(srvAllowedHostsSuffix, that.srvAllowedHostsSuffix);
}

@Override
Expand All @@ -1884,7 +1921,7 @@ public int hashCode() {
writeConcern, retryWrites, retryReads, maxAdaptiveRetries, enableOverloadRetargeting, readConcern, minConnectionPoolSize, maxConnectionPoolSize, maxWaitTime,
maxConnectionIdleTime, maxConnectionLifeTime, maxConnecting, connectTimeout, timeout, socketTimeout, sslEnabled,
sslInvalidHostnameAllowed, requiredReplicaSetName, serverSelectionTimeout, localThreshold, heartbeatFrequency,
serverMonitoringMode, applicationName, compressorList, uuidRepresentation, srvServiceName, srvMaxHosts, proxyHost,
proxyPort, proxyUsername, proxyPassword);
serverMonitoringMode, applicationName, compressorList, uuidRepresentation, srvServiceName, srvMaxHosts,
srvAllowedHostsSuffix, proxyHost, proxyPort, proxyUsername, proxyPassword);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.connection.DomainNameUtils.normalizeSrvAllowedHostsSuffix;
import static com.mongodb.internal.connection.ServerAddressHelper.createServerAddress;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
Expand All @@ -50,6 +51,7 @@ public final class ClusterSettings {
private final String srvHost;
private final Integer srvMaxHosts;
private final String srvServiceName;
private final String srvAllowedHostsSuffix;
private final List<ServerAddress> hosts;
private final ClusterConnectionMode mode;
private final ClusterType requiredClusterType;
Expand Down Expand Up @@ -88,6 +90,7 @@ public static final class Builder {
private String srvHost;
private Integer srvMaxHosts;
private String srvServiceName = "mongodb";
private String srvAllowedHostsSuffix;
private List<ServerAddress> hosts = DEFAULT_HOSTS;
private ClusterConnectionMode mode;
private ClusterType requiredClusterType = ClusterType.UNKNOWN;
Expand All @@ -114,6 +117,7 @@ public Builder applySettings(final ClusterSettings clusterSettings) {
srvHost = clusterSettings.srvHost;
srvServiceName = clusterSettings.srvServiceName;
srvMaxHosts = clusterSettings.srvMaxHosts;
srvAllowedHostsSuffix = clusterSettings.srvAllowedHostsSuffix;
hosts = clusterSettings.hosts;
mode = clusterSettings.mode;
requiredReplicaSetName = clusterSettings.requiredReplicaSetName;
Expand Down Expand Up @@ -185,6 +189,26 @@ public Builder srvServiceName(final String srvServiceName) {
return this;
}

/**
* Sets the SRV allowed hosts suffix used to validate hosts returned via SRV lookup.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*
* <p>If set, its value is used as the domain for SRV host name validation, replacing the domain inferred from
* the SRV host name. The value is normalized: a leading {@code "."} is prepended if absent, so
* {@link #getSrvAllowedHostsSuffix()} always returns a value beginning with {@code "."}. This setting is only
* used with SRV. Specifying an overly broad suffix (for example a bare TLD) weakens SRV host name validation and
* is the responsibility of the caller.</p>
*
* @param srvAllowedHostsSuffix the SRV allowed hosts suffix; may not be null or empty
* @return this
* @since 5.13
* @see #getSrvAllowedHostsSuffix()
*/
public Builder srvAllowedHostsSuffix(final String srvAllowedHostsSuffix) {
Comment thread
stIncMale marked this conversation as resolved.
notNull("srvAllowedHostsSuffix", srvAllowedHostsSuffix);
this.srvAllowedHostsSuffix = normalizeSrvAllowedHostsSuffix(srvAllowedHostsSuffix);
return this;
}

/**
* Sets the hosts for the cluster. Any duplicate server addresses are removed from the list.
*
Expand Down Expand Up @@ -328,6 +352,7 @@ public Builder applyConnectionString(final ConnectionString connectionString) {
mode(ClusterConnectionMode.LOAD_BALANCED);
if (connectionString.isSrvProtocol()) {
srvHost(connectionString.getHosts().get(0));
applySrvConnectionStringOptions(connectionString);
} else {
hosts(singletonList(createServerAddress(connectionString.getHosts().get(0))));
}
Expand All @@ -338,10 +363,7 @@ public Builder applyConnectionString(final ConnectionString connectionString) {
if (srvMaxHosts != null) {
srvMaxHosts(srvMaxHosts);
}
String srvServiceName = connectionString.getSrvServiceName();
if (srvServiceName != null) {
srvServiceName(srvServiceName);
}
applySrvConnectionStringOptions(connectionString);
} else if (directConnection != null) {
mode(directConnection ? ClusterConnectionMode.SINGLE : ClusterConnectionMode.MULTIPLE);
List<String> hosts = directConnection ? singletonList(connectionString.getHosts().get(0)) : connectionString.getHosts();
Expand All @@ -367,6 +389,19 @@ public Builder applyConnectionString(final ConnectionString connectionString) {
return this;
}

// Applies the SRV options shared by the load-balanced and multi-server SRV paths. srvMaxHosts is intentionally
// not applied here, as it is only valid for the multi-server path.
private void applySrvConnectionStringOptions(final ConnectionString connectionString) {
String srvServiceName = connectionString.getSrvServiceName();
if (srvServiceName != null) {
srvServiceName(srvServiceName);
}
String srvAllowedHostsSuffix = connectionString.getSrvAllowedHostsSuffix();
if (srvAllowedHostsSuffix != null) {
srvAllowedHostsSuffix(srvAllowedHostsSuffix);
}
}

/**
* Build the settings from the builder.
*
Expand Down Expand Up @@ -421,6 +456,23 @@ public String getSrvServiceName() {
return srvServiceName;
}

/**
* Gets the SRV allowed hosts suffix used to validate hosts returned via SRV lookup.
*
* <p>If present, its value is used as the domain for SRV host name validation, replacing the domain inferred from
* the SRV host name. The value is normalized to always begin with {@code "."}.</p>
*
* <p><b>WARNING:</b> Modifying the default SRV domain name validation can create vulnerabilities.</p>
*
* @return the normalized SRV allowed hosts suffix, always beginning with {@code "."}. Defaults to null.
* @since 5.13
* @see Builder#srvAllowedHostsSuffix(String)
*/
@Nullable
public String getSrvAllowedHostsSuffix() {
return srvAllowedHostsSuffix;
}

/**
* Gets the seed list of hosts for the cluster.
*
Expand Down Expand Up @@ -554,6 +606,7 @@ public boolean equals(final Object o) {
&& Objects.equals(srvHost, that.srvHost)
&& Objects.equals(srvMaxHosts, that.srvMaxHosts)
&& srvServiceName.equals(that.srvServiceName)
&& Objects.equals(srvAllowedHostsSuffix, that.srvAllowedHostsSuffix)
&& hosts.equals(that.hosts)
&& mode == that.mode
&& requiredClusterType == that.requiredClusterType
Expand All @@ -564,8 +617,8 @@ public boolean equals(final Object o) {

@Override
public int hashCode() {
return Objects.hash(srvHost, srvMaxHosts, srvServiceName, hosts, mode, requiredClusterType, requiredReplicaSetName, serverSelector,
localThresholdMS, serverSelectionTimeoutMS, clusterListeners);
return Objects.hash(srvHost, srvMaxHosts, srvServiceName, srvAllowedHostsSuffix, hosts, mode, requiredClusterType,
requiredReplicaSetName, serverSelector, localThresholdMS, serverSelectionTimeoutMS, clusterListeners);
}

@Override
Expand All @@ -575,6 +628,7 @@ public String toString() {
+ (srvHost == null ? "" : ", srvHost=" + srvHost)
+ (srvServiceName == null ? "" : ", srvServiceName=" + srvServiceName)
+ (srvMaxHosts == null ? "" : ", srvMaxHosts=" + srvMaxHosts)
+ (srvAllowedHostsSuffix == null ? "" : ", srvAllowedHostsSuffix=" + srvAllowedHostsSuffix)
+ ", mode=" + mode
+ ", requiredClusterType=" + requiredClusterType
+ ", requiredReplicaSetName='" + requiredReplicaSetName + '\''
Expand Down Expand Up @@ -624,6 +678,7 @@ private ClusterSettings(final Builder builder) {
srvHost = builder.srvHost;
srvMaxHosts = builder.srvMaxHosts;
srvServiceName = builder.srvServiceName;
srvAllowedHostsSuffix = builder.srvAllowedHostsSuffix;
hosts = builder.hosts;
requiredReplicaSetName = builder.requiredReplicaSetName;
if (builder.mode != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.mongodb.internal.diagnostics.logging.Logger;
import com.mongodb.internal.diagnostics.logging.Loggers;
import com.mongodb.internal.dns.DnsResolver;
import com.mongodb.lang.Nullable;

import java.util.Collections;
import java.util.HashSet;
Expand All @@ -38,18 +39,22 @@ class DefaultDnsSrvRecordMonitor implements DnsSrvRecordMonitor {

private final String hostName;
private final String srvServiceName;
@Nullable
private final String srvAllowedHostsSuffix;
private final long rescanFrequencyMillis;
private final long noRecordsRescanFrequencyMillis;
private final DnsSrvRecordInitializer dnsSrvRecordInitializer;
private final DnsResolver dnsResolver;
private final Thread monitorThread;
private volatile boolean isClosed;

DefaultDnsSrvRecordMonitor(final String hostName, final String srvServiceName, final long rescanFrequencyMillis, final long noRecordsRescanFrequencyMillis,
DefaultDnsSrvRecordMonitor(final String hostName, final String srvServiceName, @Nullable final String srvAllowedHostsSuffix,
final long rescanFrequencyMillis, final long noRecordsRescanFrequencyMillis,
final DnsSrvRecordInitializer dnsSrvRecordInitializer, final ClusterId clusterId,
final DnsResolver dnsResolver) {
this.hostName = hostName;
this.srvServiceName = srvServiceName;
this.srvAllowedHostsSuffix = srvAllowedHostsSuffix;
this.rescanFrequencyMillis = rescanFrequencyMillis;
this.noRecordsRescanFrequencyMillis = noRecordsRescanFrequencyMillis;
this.dnsSrvRecordInitializer = dnsSrvRecordInitializer;
Expand Down Expand Up @@ -78,7 +83,8 @@ public void run() {
try {
while (!isClosed && shouldContinueMonitoring()) {
try {
List<String> resolvedHostNames = dnsResolver.resolveHostFromSrvRecords(hostName, srvServiceName);
List<String> resolvedHostNames = dnsResolver.resolveHostFromSrvRecords(hostName, srvServiceName,
srvAllowedHostsSuffix);
Set<ServerAddress> hosts = createServerAddressSet(resolvedHostNames);

if (isClosed) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ public DefaultDnsSrvRecordMonitorFactory(final ClusterId clusterId, final Server
}

@Override
public DnsSrvRecordMonitor create(final String hostName, final String srvServiceName, final DnsSrvRecordInitializer dnsSrvRecordInitializer) {
return new DefaultDnsSrvRecordMonitor(hostName, srvServiceName, DEFAULT_RESCAN_FREQUENCY_MILLIS, noRecordsRescanFrequency,
dnsSrvRecordInitializer, clusterId, new DefaultDnsResolver(dnsClient));
public DnsSrvRecordMonitor create(final String hostName, final String srvServiceName,
@Nullable final String srvAllowedHostsSuffix, final DnsSrvRecordInitializer dnsSrvRecordInitializer) {
return new DefaultDnsSrvRecordMonitor(hostName, srvServiceName, srvAllowedHostsSuffix, DEFAULT_RESCAN_FREQUENCY_MILLIS,
noRecordsRescanFrequency, dnsSrvRecordInitializer, clusterId, new DefaultDnsResolver(dnsClient));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public DnsMultiServerCluster(final ClusterId clusterId, final ClusterSettings se
final DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory) {
super(clusterId, settings, serverFactory, clientMetadata);
dnsSrvRecordMonitor = dnsSrvRecordMonitorFactory.create(assertNotNull(settings.getSrvHost()), settings.getSrvServiceName(),
settings.getSrvAllowedHostsSuffix(),
new DnsSrvRecordInitializer() {
private volatile boolean initialized;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

package com.mongodb.internal.connection;

import com.mongodb.lang.Nullable;

/**
* <p>This class is not part of the public API and may be removed or changed at any time</p>
*/
public interface DnsSrvRecordMonitorFactory {
DnsSrvRecordMonitor create(String hostName, String srvServiceName, DnsSrvRecordInitializer dnsSrvRecordInitializer);
DnsSrvRecordMonitor create(String hostName, String srvServiceName, @Nullable String srvAllowedHostsSuffix,
DnsSrvRecordInitializer dnsSrvRecordInitializer);
}
Loading