Skip to content

Commit

Permalink
feat(NODE-6289): allow valid srv hostnames with less than 3 parts (#4197
Browse files Browse the repository at this point in the history
)
  • Loading branch information
aditi-khare-mongoDB authored Oct 15, 2024
1 parent 7fde8dd commit 3d5bd51
Show file tree
Hide file tree
Showing 5 changed files with 406 additions and 50 deletions.
11 changes: 2 additions & 9 deletions src/connection_string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ import { ReadPreference, type ReadPreferenceMode } from './read_preference';
import { ServerMonitoringMode } from './sdam/monitor';
import type { TagSet } from './sdam/server_description';
import {
checkParentDomainMatch,
DEFAULT_PK_FACTORY,
emitWarning,
HostAddress,
isRecord,
matchesParentDomain,
parseInteger,
setDifference,
squashError
Expand All @@ -64,11 +64,6 @@ export async function resolveSRVRecord(options: MongoOptions): Promise<HostAddre
throw new MongoAPIError('Option "srvHost" must not be empty');
}

if (options.srvHost.split('.').length < 3) {
// TODO(NODE-3484): Replace with MongoConnectionStringError
throw new MongoAPIError('URI must include hostname, domain name, and tld');
}

// Asynchronously start TXT resolution so that we do not have to wait until
// the SRV record is resolved before starting a second DNS query.
const lookupAddress = options.srvHost;
Expand All @@ -86,9 +81,7 @@ export async function resolveSRVRecord(options: MongoOptions): Promise<HostAddre
}

for (const { name } of addresses) {
if (!matchesParentDomain(name, lookupAddress)) {
throw new MongoAPIError('Server record does not share hostname with parent URI');
}
checkParentDomainMatch(name, lookupAddress);
}

const hostAddresses = addresses.map(r => HostAddress.fromString(`${r.name}:${r.port ?? 27017}`));
Expand Down
7 changes: 5 additions & 2 deletions src/sdam/srv_polling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { clearTimeout, setTimeout } from 'timers';

import { MongoRuntimeError } from '../error';
import { TypedEventEmitter } from '../mongo_types';
import { HostAddress, matchesParentDomain, squashError } from '../utils';
import { checkParentDomainMatch, HostAddress, squashError } from '../utils';

/**
* @internal
Expand Down Expand Up @@ -127,8 +127,11 @@ export class SrvPoller extends TypedEventEmitter<SrvPollerEvents> {

const finalAddresses: dns.SrvRecord[] = [];
for (const record of srvRecords) {
if (matchesParentDomain(record.name, this.srvHost)) {
try {
checkParentDomainMatch(record.name, this.srvHost);
finalAddresses.push(record);
} catch (error) {
squashError(error);
}
}

Expand Down
29 changes: 24 additions & 5 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { FindCursor } from './cursor/find_cursor';
import type { Db } from './db';
import {
type AnyError,
MongoAPIError,
MongoCompatibilityError,
MongoInvalidArgumentError,
MongoNetworkTimeoutError,
Expand Down Expand Up @@ -1142,29 +1143,47 @@ export function parseUnsignedInteger(value: unknown): number | null {
}

/**
* Determines whether a provided address matches the provided parent domain.
* This function throws a MongoAPIError in the event that either of the following is true:
* * If the provided address domain does not match the provided parent domain
* * If the parent domain contains less than three `.` separated parts and the provided address does not contain at least one more domain level than its parent
*
* If a DNS server were to become compromised SRV records would still need to
* advertise addresses that are under the same domain as the srvHost.
*
* @param address - The address to check against a domain
* @param srvHost - The domain to check the provided address against
* @returns Whether the provided address matches the parent domain
* @returns void
*/
export function matchesParentDomain(address: string, srvHost: string): boolean {
export function checkParentDomainMatch(address: string, srvHost: string): void {
// Remove trailing dot if exists on either the resolved address or the srv hostname
const normalizedAddress = address.endsWith('.') ? address.slice(0, address.length - 1) : address;
const normalizedSrvHost = srvHost.endsWith('.') ? srvHost.slice(0, srvHost.length - 1) : srvHost;

const allCharacterBeforeFirstDot = /^.*?\./;
const srvIsLessThanThreeParts = normalizedSrvHost.split('.').length < 3;
// Remove all characters before first dot
// Add leading dot back to string so
// an srvHostDomain = '.trusted.site'
// will not satisfy an addressDomain that endsWith '.fake-trusted.site'
const addressDomain = `.${normalizedAddress.replace(allCharacterBeforeFirstDot, '')}`;
const srvHostDomain = `.${normalizedSrvHost.replace(allCharacterBeforeFirstDot, '')}`;
let srvHostDomain = srvIsLessThanThreeParts
? normalizedSrvHost
: `.${normalizedSrvHost.replace(allCharacterBeforeFirstDot, '')}`;

return addressDomain.endsWith(srvHostDomain);
if (!srvHostDomain.startsWith('.')) {
srvHostDomain = '.' + srvHostDomain;
}
if (
srvIsLessThanThreeParts &&
normalizedAddress.split('.').length <= normalizedSrvHost.split('.').length
) {
throw new MongoAPIError(
'Server record does not have at least one more domain level than parent URI'
);
}
if (!addressDomain.endsWith(srvHostDomain)) {
throw new MongoAPIError('Server record does not share hostname with parent URI');
}
}

interface RequestOptions {
Expand Down
Loading

0 comments on commit 3d5bd51

Please sign in to comment.