From 09c55d71a240a007478693ac733f5d4c35d02630 Mon Sep 17 00:00:00 2001 From: Jaeseung Lee <41176085+tkxkd0159@users.noreply.github.com> Date: Fri, 21 Nov 2025 00:47:05 +0900 Subject: [PATCH 1/5] fix: normalize domains to ensure FQDN equality --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4346584..2982881 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,8 +15,8 @@ const ALLOWED_TXT_OPTIONS: Readonly = ['authSource', 'replicaSet', 'lo function matchesParentDomain (srvAddress: string, parentDomain: string): boolean { const regex = /^.*?\./; - const srv = `.${srvAddress.replace(regex, '')}`; - const parent = `.${parentDomain.replace(regex, '')}`; + const srv = `.${(srvAddress.endsWith(".") ? srvAddress.slice(0, -1) : srvAddress).replace(regex, "")}`; + const parent = `.${(parentDomain.endsWith(".") ? parentDomain.slice(0, -1) : parentDomain).replace(regex, "")}`; return srv.endsWith(parent); } From e9d667fc9169430230096cd550f75010d9b3047a Mon Sep 17 00:00:00 2001 From: Jaeseung Lee <41176085+tkxkd0159@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:38:39 +0900 Subject: [PATCH 2/5] fix lint --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2982881..8df6be9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,8 +15,8 @@ const ALLOWED_TXT_OPTIONS: Readonly = ['authSource', 'replicaSet', 'lo function matchesParentDomain (srvAddress: string, parentDomain: string): boolean { const regex = /^.*?\./; - const srv = `.${(srvAddress.endsWith(".") ? srvAddress.slice(0, -1) : srvAddress).replace(regex, "")}`; - const parent = `.${(parentDomain.endsWith(".") ? parentDomain.slice(0, -1) : parentDomain).replace(regex, "")}`; + const srv = `.${(srvAddress.endsWith('.') ? srvAddress.slice(0, -1) : srvAddress).replace(regex, '')}`; + const parent = `.${(parentDomain.endsWith('.') ? parentDomain.slice(0, -1) : parentDomain).replace(regex, '')}`; return srv.endsWith(parent); } From 0cc4b972a05a4a327cf64d70745207edf387b9a0 Mon Sep 17 00:00:00 2001 From: Jaeseung Lee <41176085+tkxkd0159@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:46:30 +0900 Subject: [PATCH 3/5] Apply feedback --- src/index.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8df6be9..fe7471a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,11 +13,24 @@ type Options = { const ALLOWED_TXT_OPTIONS: Readonly = ['authSource', 'replicaSet', 'loadBalanced']; -function matchesParentDomain (srvAddress: string, parentDomain: string): boolean { - const regex = /^.*?\./; - const srv = `.${(srvAddress.endsWith('.') ? srvAddress.slice(0, -1) : srvAddress).replace(regex, '')}`; - const parent = `.${(parentDomain.endsWith('.') ? parentDomain.slice(0, -1) : parentDomain).replace(regex, '')}`; - return srv.endsWith(parent); +function matchesParentDomain (address: string, parentDomain: string): void { + const normalize = (s: string): string => (s.endsWith('.') ? s.slice(0, -1) : s); + const addr = normalize(address); + const parent = normalize(parentDomain); + + const addrParts = addr.split('.'); + const parentParts = parent.split('.'); + const isParentShort = parentParts.length < 3; + if (isParentShort && addrParts.length <= parentParts.length) { + throw new MongoParseError('Server record does not have at least one more domain level than parent URI'); + } + + // Prevent insecure "TLD-only matching" on short domains + const requiredSuffix = `.${parentParts.slice(isParentShort ? 0 : 1).join('.')}`; + const addrSuffix = `.${addrParts.slice(1).join('.')}`; + if (!addrSuffix.endsWith(requiredSuffix)) { + throw new MongoParseError('Server record does not share hostname with parent URI'); + } } async function resolveDnsSrvRecord (dns: NonNullable, lookupAddress: string, srvServiceName: string): Promise { @@ -27,9 +40,7 @@ async function resolveDnsSrvRecord (dns: NonNullable, lookupAddr } for (const { name } of addresses) { - if (!matchesParentDomain(name, lookupAddress)) { - throw new MongoParseError('Server record does not share hostname with parent URI'); - } + matchesParentDomain(name, lookupAddress); } return addresses.map(r => r.name + ((r.port ?? 27017) === 27017 ? '' : `:${r.port}`)); From aa6a5b323dd31b6e095240340d1c5def170d92b2 Mon Sep 17 00:00:00 2001 From: Jaeseung Lee <41176085+tkxkd0159@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:40:33 +0900 Subject: [PATCH 4/5] add test --- test/index.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/index.ts b/test/index.ts index 1968c99..6ee6084 100644 --- a/test/index.ts +++ b/test/index.ts @@ -225,6 +225,34 @@ describe('resolveMongodbSrv', () => { await resolveMongodbSrv('mongodb+srv://server.example.com/?srvMaxHosts=1', { dns }), /^mongodb:\/\/host[1-3]\.example\.com\/\?tls=true$/); }); + + it('rejects SRV records without additional subdomain when parent domain has fewer than 3 parts', async () => { + txtResult = []; + srvResult = [{ name: 'example.com', port: 27017 }]; + await assert.rejects(resolveMongodbSrv('mongodb+srv://example.com', { dns })); + }); + + it('allow trailing dot in SRV lookup', async () => { + txtResult = []; + srvResult = [ + { name: 'asdf.example.com', port: 27017 }, + { name: 'meow.example.com', port: 27017 } + ]; + assert.strictEqual( + await resolveMongodbSrv('mongodb+srv://server.example.com.', { dns }), + 'mongodb://asdf.example.com,meow.example.com/?tls=true'); + + srvResult = [ + { name: 'asdf.example.com.', port: 27017 }, + { name: 'meow.example.com', port: 27017 } + ]; + assert.strictEqual( + await resolveMongodbSrv('mongodb+srv://server.example.com', { dns }), + 'mongodb://asdf.example.com.,meow.example.com/?tls=true'); + assert.strictEqual( + await resolveMongodbSrv('mongodb+srv://server.example.com.', { dns }), + 'mongodb://asdf.example.com.,meow.example.com/?tls=true'); + }); }); for (const [name, dnsProvider] of [ From 9d75f0430c4eb14976edd3555c721893bb6f0fe5 Mon Sep 17 00:00:00 2001 From: Jaeseung Lee <41176085+tkxkd0159@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:52:41 +0900 Subject: [PATCH 5/5] add await for asynchronous assertions --- test/index.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/test/index.ts b/test/index.ts index 6ee6084..a5dde78 100644 --- a/test/index.ts +++ b/test/index.ts @@ -49,26 +49,26 @@ describe('resolveMongodbSrv', () => { }); it('rejects non-mongodb schemes', async () => { - assert.rejects(resolveMongodbSrv('http://somewhere.example.com', { dns })); + await assert.rejects(resolveMongodbSrv('http://somewhere.example.com', { dns })); }); it('rejects mongodb+srv with port', async () => { - assert.rejects(resolveMongodbSrv('mongodb+srv://somewhere.example.com:27017', { dns })); + await assert.rejects(resolveMongodbSrv('mongodb+srv://somewhere.example.com:27017', { dns })); }); it('rejects when the SRV lookup rejects', async () => { srvError = new Error(); - assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); + await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); }); it('rejects when the SRV lookup returns no results', async () => { srvResult = []; - assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); + await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); }); it('rejects when the SRV lookup returns foreign hostnames', async () => { srvResult = [{ name: 'server.example.org', port: 27017 }]; - assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); + await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); }); it('respects SRV-provided ports', async () => { @@ -81,7 +81,7 @@ describe('resolveMongodbSrv', () => { it('rejects when the TXT lookup rejects with a fatal error', async () => { srvResult = [{ name: 'asdf.example.com', port: 27017 }]; txtError = Object.assign(new Error(), { code: 'ENOENT' }); - assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); + await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); }); it('does not reject when the TXT lookup results in ENOTFOUND', async () => { @@ -103,13 +103,13 @@ describe('resolveMongodbSrv', () => { it('rejects when the TXT lookup returns more than one result', async () => { srvResult = [{ name: 'asdf.example.com', port: 27017 }]; txtResult = [['a'], ['b']]; - assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); + await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); }); it('rejects when the TXT lookup returns invalid connection string options', async () => { srvResult = [{ name: 'asdf.example.com', port: 27017 }]; txtResult = [['a=b']]; - assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); + await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); }); it('accepts TXT lookup authSource', async () => { @@ -123,7 +123,7 @@ describe('resolveMongodbSrv', () => { it('rejects empty TXT lookup authSource', async () => { srvResult = [{ name: 'asdf.example.com', port: 27017 }]; txtResult = [['authSource=']]; - assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); + await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); }); it('prioritizes URL-provided over TXT lookup authSource', async () => { @@ -145,7 +145,7 @@ describe('resolveMongodbSrv', () => { it('rejects empty TXT lookup replicaSet', async () => { srvResult = [{ name: 'asdf.example.com', port: 27017 }]; txtResult = [['replicaSet=']]; - assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); + await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); }); it('prioritizes URL-provided over TXT lookup replicaSet', async () => { @@ -174,13 +174,13 @@ describe('resolveMongodbSrv', () => { it('rejects empty TXT lookup loadBalanced', async () => { srvResult = [{ name: 'asdf.example.com', port: 27017 }]; txtResult = [['loadBalanced=']]; - assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); + await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); }); it('rejects non true/false TXT lookup loadBalanced', async () => { srvResult = [{ name: 'asdf.example.com', port: 27017 }]; txtResult = [['loadBalanced=bla']]; - assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); + await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns })); }); it('prioritizes URL-provided over TXT lookup loadBalanced', async () => { @@ -232,6 +232,12 @@ describe('resolveMongodbSrv', () => { await assert.rejects(resolveMongodbSrv('mongodb+srv://example.com', { dns })); }); + it('not strip first subdomain when parent domain has fewer than 3 part to prevent TLD-only matching', async () => { + txtResult = []; + srvResult = [{ name: 'asdf.malicious.com', port: 27017 }]; + await assert.rejects(resolveMongodbSrv('mongodb+srv://example.com', { dns })); + }); + it('allow trailing dot in SRV lookup', async () => { txtResult = []; srvResult = [