Conversation
Add a MongoClient-level `srvAllowedHostsSuffix` option that overrides the domain inferred from the SRV host name when validating the hosts returned by an SRV lookup. The value is normalized at configuration time (a leading "." is prepended if absent, so the stored and returned value always begins with "."), and every resolved host must end with it, matched case-insensitively; otherwise the existing inferred-domain validation is unchanged. The option is parsed from the connection string, carried on ClusterSettings, rejected on non-mongodb+srv URIs, rejected when it contains whitespace, no domain label, or an empty domain label, and threaded through the internal DNS resolution path. It is supported with both multi-server and load-balanced SRV connection strings, which now also apply srvServiceName consistently. JAVA-6233
There was a problem hiding this comment.
🟡 Changes recommended
Case, IDN, and platform-charset handling currently permit public-suffix deny-list bypasses.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds configurable SRV hostname-suffix validation across connection parsing, cluster configuration, DNS resolution, and monitoring.
Changes:
- Adds
srvAllowedHostsSuffixto public configuration APIs. - Propagates and enforces the suffix during SRV discovery.
- Adds unit, functional, and prose-test coverage.
File summaries
| File | Description |
|---|---|
driver-sync/src/test/functional/com/mongodb/client/InitialDnsSeedlistDiscoveryTest.java |
Supports the new option in functional assertions. |
driver-core/src/test/unit/com/mongodb/internal/dns/DefaultDnsResolverTest.java |
Tests suffix-based DNS validation. |
driver-core/src/test/unit/com/mongodb/internal/connection/SrvPollingProseTests.java |
Updates resolver and monitor signatures. |
driver-core/src/test/unit/com/mongodb/internal/connection/LoadBalancedClusterTest.java |
Updates monitor factory mocks. |
driver-core/src/test/unit/com/mongodb/internal/connection/InitialDnsSeedListDiscoveryProseTest.java |
Tests configured suffix overrides. |
driver-core/src/test/unit/com/mongodb/internal/connection/DnsMultiServerClusterSpecification.groovy |
Updates factory implementation signature. |
driver-core/src/test/unit/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitorSpecification.groovy |
Updates monitor tests for suffix propagation. |
driver-core/src/test/unit/com/mongodb/ConnectionStringUnitTest.java |
Tests parsing and validation. |
driver-core/src/test/unit/com/mongodb/connection/ClusterSettingsSpecification.groovy |
Tests builder and URI application. |
driver-core/src/test/unit/com/mongodb/AbstractConnectionStringTest.java |
Supports the option in shared tests. |
driver-core/src/main/com/mongodb/internal/dns/DnsResolver.java |
Extends SRV resolution contract. |
driver-core/src/main/com/mongodb/internal/dns/DefaultDnsResolver.java |
Enforces suffix validation. |
driver-core/src/main/com/mongodb/internal/connection/LoadBalancedCluster.java |
Propagates suffix for load-balanced clusters. |
driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java |
Normalizes suffixes and checks the PSL. |
driver-core/src/main/com/mongodb/internal/connection/DnsSrvRecordMonitorFactory.java |
Extends monitor factory contract. |
driver-core/src/main/com/mongodb/internal/connection/DnsMultiServerCluster.java |
Propagates suffix for multi-server clusters. |
driver-core/src/main/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitorFactory.java |
Passes suffix into monitors. |
driver-core/src/main/com/mongodb/internal/connection/DefaultDnsSrvRecordMonitor.java |
Applies suffix during polling. |
driver-core/src/main/com/mongodb/ConnectionString.java |
Adds URI parsing and public accessor. |
driver-core/src/main/com/mongodb/connection/ClusterSettings.java |
Adds builder and settings support. |
Review details
Suppressed comments (2)
driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java:79
- The bundled PSL is UTF-8 and contains non-ASCII rules such as
公司.cn, but this constructor decodes it with the JVM default charset. On non-UTF-8 Java 8 environments those rules are corrupted, so equivalent Unicode public suffixes can bypass the deny list. Specify UTF-8 explicitly.
try (Scanner scanner = new Scanner(Objects.requireNonNull(
DomainNameUtils.class.getResourceAsStream("public_suffix_list.dat"), "Missing DNS suffix list"))) {
driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java:63
- This condition rejects multi-label and private public-suffix-list entries as well as actual top-level domains, so this message is misleading for inputs such as
.co.uk. Describe the rejected value as a public suffix and update the assertions that currently expect this text.
throw new IllegalArgumentException("srvAllowedHostsSuffix must not be a top-level domain");
- Files reviewed: 20/21 changed files
- Comments generated: 3
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| throw new IllegalArgumentException("srvAllowedHostsSuffix must not contain empty domain labels"); | ||
| } | ||
| } | ||
| if (isTopLevelDomain(hasLeadingDot ? srvAllowedHostsSuffix.substring(1) : srvAllowedHostsSuffix)) { |
| ConnectionString first = new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=.build.10gen.cc"); | ||
| ConnectionString second = new ConnectionString("mongodb+srv://test12.test.build.10gen.cc/?srvAllowedHostsSuffix=.other.10gen.cc"); | ||
| assertNotEquals(first, second); | ||
| assertNotEquals(first.hashCode(), second.hashCode()); |
There was a problem hiding this comment.
🟡 Changes recommended
Case-sensitive public-suffix validation permits uppercase deny-list bypasses, and the API documentation contradicts implemented restrictions.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
driver-core/src/main/com/mongodb/internal/connection/DomainNameUtils.java:62
- The public-suffix lookup is case-sensitive even though DNS suffixes are case-insensitive. A URI using
.COMbypasses this safeguard, while the resolver later accepts.COMagainst any.comtarget. Normalize the value for the PSL lookup so uppercase variants are rejected too.
if (isTopLevelDomain(hasLeadingDot ? srvAllowedHostsSuffix.substring(1) : srvAllowedHostsSuffix)) {
driver-core/src/main/com/mongodb/connection/ClusterSettings.java:199
- The public builder documentation says a bare TLD is accepted at the caller's risk, but
normalizeSrvAllowedHostsSuffixrejects it (and every other public suffix). This makes the documented API contract disagree with actual behavior.
* 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>
- Files reviewed: 20/21 changed files
- Comments generated: 2
- Review effort level: Balanced
| } | ||
| } | ||
| if (isTopLevelDomain(hasLeadingDot ? srvAllowedHostsSuffix.substring(1) : srvAllowedHostsSuffix)) { | ||
| throw new IllegalArgumentException("srvAllowedHostsSuffix must not be a top-level domain"); |
| * is stored and returned to callers. The suffix must contain at least one non-empty domain label and no | ||
| * whitespace; an overly broad suffix is permitted and is the caller's responsibility. |
nhachicha
left a comment
There was a problem hiding this comment.
Thanks for taking this over! The implementation in general is ok, a couple of things left:
-
- The official spec tests never run. The submodule isn't bumped.
-
public_suffix_list.datThe PSL spec says drivers MUST obtain it from the specifications repo, where it's pre-stripped of comments (partially my fault since I pointed you to mongodb/mongo-python-driver#2903 instead of mongodb/mongo-python-driver#2868 which contains the correct file)
-
- Claude
/driver-code-reviewcaught some additional points, please assess and pushback / ignore if they're irrelevant
- Claude
Claude Opus (1M Context) review
🔴 Blocking
DOMAIN_PATTERNwidening is a regression. Loosening the TLD class from[a-zA-Z]{2,63}to[a-zA-Z0-9\-]{2,63}to admitxn--labels changes a shared method.xn--frosch-6ya.w23is an existing negative case inDomainNameUtilsTest, so that test now fails; andSocksSocket:135usesisDomainNamefor SOCKS5 proxy hosts, which now acceptsfoo.123,foo.--,evil.-a-. Suggest validating with the original pattern before IDN conversion, or special-casing thexn--prefix.- MPL-2.0 attribution missing. The vendored list ships in the Apache-2.0
driver-corejar; neitherTHIRD-PARTY-NOTICESnorsbom.jsonis updated. Needs a notice entry and legal/human sign-off.
🟡 Important
- Trailing/leading dot not stripped (spec step 1).
.build.10gen.cc.currently throws; the spec requires stripping, andsrvAllowedHostsSuffix-trailing-dotexpects success. The current test enshrines the wrong behaviour. isPublicSuffixisn't the publicsuffix.org algorithm. TheinvalidMatchWildcardlatch converts toreturn trueon the next ordinary line and is never reset, so results depend on upstream line ordering (it works only because!www.cksits immediately after*.ck). Suggest a rule index + the documented algorithm: exceptions win, then longest match.- Per-call cost got worse.
IDN.toASCIInow runs on every rule line — ~16.4k conversions per validation, doubled becauseConnectionStringandClusterSettings.Builderboth normalize. An ASCII fast-path plus a lazily-built index fixes both. - Misleading error messages.
.comfails with "is not a valid domain" (it tripsisDomainNameas a single label) rather than the public-suffix message; for bare TLDs the PSL check is effectively unreachable. ClusterSettings.Builderjavadoc still contradicts the code (Copilot's comment, unaddressed) — says a bare TLD is the caller's responsibility, but public suffixes are rejected. TheConnectionStringoption docs also carry no security warning, which is the path most users take.- New Spock tests in
ClusterSettingsSpecification.groovy—.agents/references/testing-guide.md: "Do not add new Spock tests." Mechanical signature updates are fine; the new blocks should be JUnit 5. - No direct test for the reworked matcher.
DomainNameUtilsTestalready exists — add uppercase, multi-label public suffixes, wildcard/exception ordering, U-label vs A-label, and missing-resource cases. - Out-of-scope behaviour changes.
applySrvConnectionStringOptionsnow appliessrvServiceNameon the LOAD_BALANCED path for the first time (genuine fix, but unrelated), and the unwrappedMongoConfigurationExceptionrethrow changes messages on the pre-existing inferred-domain path. Please split or call out in the description. ClusterSettings.build()doesn't reject the option without SRV, unlike the URI path.
🟢 Nit
assertNotEquals(first.hashCode(), second.hashCode())asserts more than thehashCodecontract (Copilot's comment, unaddressed).DOMAIN_PATTERNallowslocalhost, sosrvAllowedHostsSuffix=localhostslips past the ≥2-label rule.- Duplicated comment on the two wildcard tests — inaccurate on the first.
- Stray double blank line in
DefaultDnsResolverTest; over-length lines inLoadBalancedClusterTest. Builder.srvAllowedHostsSuffixcan't be reset to null; field lacks@Nullablethough the getter has it.
| * always begins with {@code "."}.</p> | ||
| * | ||
| * @return the normalized SRV allowed hosts suffix, always beginning with {@code "."}. Defaults to null. | ||
| * @since 5.9 |
There was a problem hiding this comment.
The feature will probably be released in 5.13 (next minor)
| * @since 5.9 | |
| * @since 5.13 |
| if (labels.isEmpty()) { | ||
| throw new IllegalArgumentException("srvAllowedHostsSuffix must contain at least one domain label"); | ||
| } | ||
| String[] parts = labels.split("\\.", -1); |
There was a problem hiding this comment.
I believe this is redundant since the check below isDomainName rejects "build..10gen.cc" you can keep it if you think this gives more clarity (specific error message)
| private static boolean isPublicSuffix(final String suffix) { | ||
| try (Scanner scanner = new Scanner( | ||
| Objects.requireNonNull( | ||
| DomainNameUtils.class.getResourceAsStream("public_suffix_list.dat"), "Missing DNS suffix list"), |
There was a problem hiding this comment.
| DomainNameUtils.class.getResourceAsStream("public_suffix_list.dat"), "Missing DNS suffix list"), | |
| DomainNameUtils.class.getResourceAsStream("public_suffix_list.dat"), "Missing DNS public suffix list"), |
| private static boolean isPublicSuffix(final String suffix) { | ||
| try (Scanner scanner = new Scanner( | ||
| Objects.requireNonNull( | ||
| DomainNameUtils.class.getResourceAsStream("public_suffix_list.dat"), "Missing DNS suffix list"), |
There was a problem hiding this comment.
Caught by Claude:
GraalVM: public_suffix_list.dat isn't registered as a resource
Native image drops unregistered classpath resources, so getResourceAsStream("public_suffix_list.dat") will NPE for anyone setting srvAllowedHostsSuffix. We hand-maintain this metadata. Add to driver-core/src/main/resources/META-INF/native-image/resource-config.json:
{ "pattern":"\\Qcom/mongodb/internal/connection/public_suffix_list.dat\\E" }
Note graalvm-native-image-app-task passes either way today — missing resources fail at runtime on access, not at build, and NativeImageApp never hits this path. Worth adding a small component beside DnsSpi that builds a ConnectionString with the option set (no server needed) so CI can actually catch this.
| boolean invalidMatchWildcard = false; | ||
| while (scanner.hasNextLine()) { | ||
| String line = scanner.nextLine(); | ||
| if (line.startsWith("//") || line.isEmpty()) { |
There was a problem hiding this comment.
You shouldn't need this logic if we're using the public_suffix_list.data provisioned from spec
Drivers SHOULD NOT fetch the list from the network at runtime, and SHOULD instead resolve it from a copy shipped with the driver.
| * | ||
| * @param srvAllowedHostsSuffix the SRV allowed hosts suffix; may not be null or empty | ||
| * @return this | ||
| * @since 5.9 |
There was a problem hiding this comment.
| * @since 5.9 | |
| * @since 5.13 |
| @Test | ||
| public void shouldThrowWhenAnyResolvedHostDoesNotEndWithSrvAllowedHostsSuffix() { | ||
| DefaultDnsResolver resolver = resolverReturning("ok.build.10gen.cc.", "bad.evil.example.com."); | ||
| assertThrows(MongoConfigurationException.class, |
There was a problem hiding this comment.
should we assert the message include bad.evil.example.com.? Making sure the exception include it for investigation/Root Cause Analysis...
Add support for
srvAllowedHostsSuffixusing the public suffix list as a deny list as a safeguard and add docs explaining the use of this parameter (and its potential security implications when used).