Skip to content

Commit 5982169

Browse files
authored
fix: normalize domains to ensure FQDN equality (#6)
1 parent 2c37c13 commit 5982169

File tree

2 files changed

+65
-20
lines changed

2 files changed

+65
-20
lines changed

src/index.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,24 @@ type Options = {
1313

1414
const ALLOWED_TXT_OPTIONS: Readonly<string[]> = ['authSource', 'replicaSet', 'loadBalanced'];
1515

16-
function matchesParentDomain (srvAddress: string, parentDomain: string): boolean {
17-
const regex = /^.*?\./;
18-
const srv = `.${srvAddress.replace(regex, '')}`;
19-
const parent = `.${parentDomain.replace(regex, '')}`;
20-
return srv.endsWith(parent);
16+
function matchesParentDomain (address: string, parentDomain: string): void {
17+
const normalize = (s: string): string => (s.endsWith('.') ? s.slice(0, -1) : s);
18+
const addr = normalize(address);
19+
const parent = normalize(parentDomain);
20+
21+
const addrParts = addr.split('.');
22+
const parentParts = parent.split('.');
23+
const isParentShort = parentParts.length < 3;
24+
if (isParentShort && addrParts.length <= parentParts.length) {
25+
throw new MongoParseError('Server record does not have at least one more domain level than parent URI');
26+
}
27+
28+
// Prevent insecure "TLD-only matching" on short domains
29+
const requiredSuffix = `.${parentParts.slice(isParentShort ? 0 : 1).join('.')}`;
30+
const addrSuffix = `.${addrParts.slice(1).join('.')}`;
31+
if (!addrSuffix.endsWith(requiredSuffix)) {
32+
throw new MongoParseError('Server record does not share hostname with parent URI');
33+
}
2134
}
2235

2336
async function resolveDnsSrvRecord (dns: NonNullable<Options['dns']>, lookupAddress: string, srvServiceName: string): Promise<string[]> {
@@ -27,9 +40,7 @@ async function resolveDnsSrvRecord (dns: NonNullable<Options['dns']>, lookupAddr
2740
}
2841

2942
for (const { name } of addresses) {
30-
if (!matchesParentDomain(name, lookupAddress)) {
31-
throw new MongoParseError('Server record does not share hostname with parent URI');
32-
}
43+
matchesParentDomain(name, lookupAddress);
3344
}
3445

3546
return addresses.map(r => r.name + ((r.port ?? 27017) === 27017 ? '' : `:${r.port}`));

test/index.ts

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,26 +49,26 @@ describe('resolveMongodbSrv', () => {
4949
});
5050

5151
it('rejects non-mongodb schemes', async () => {
52-
assert.rejects(resolveMongodbSrv('http://somewhere.example.com', { dns }));
52+
await assert.rejects(resolveMongodbSrv('http://somewhere.example.com', { dns }));
5353
});
5454

5555
it('rejects mongodb+srv with port', async () => {
56-
assert.rejects(resolveMongodbSrv('mongodb+srv://somewhere.example.com:27017', { dns }));
56+
await assert.rejects(resolveMongodbSrv('mongodb+srv://somewhere.example.com:27017', { dns }));
5757
});
5858

5959
it('rejects when the SRV lookup rejects', async () => {
6060
srvError = new Error();
61-
assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
61+
await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
6262
});
6363

6464
it('rejects when the SRV lookup returns no results', async () => {
6565
srvResult = [];
66-
assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
66+
await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
6767
});
6868

6969
it('rejects when the SRV lookup returns foreign hostnames', async () => {
7070
srvResult = [{ name: 'server.example.org', port: 27017 }];
71-
assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
71+
await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
7272
});
7373

7474
it('respects SRV-provided ports', async () => {
@@ -81,7 +81,7 @@ describe('resolveMongodbSrv', () => {
8181
it('rejects when the TXT lookup rejects with a fatal error', async () => {
8282
srvResult = [{ name: 'asdf.example.com', port: 27017 }];
8383
txtError = Object.assign(new Error(), { code: 'ENOENT' });
84-
assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
84+
await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
8585
});
8686

8787
it('does not reject when the TXT lookup results in ENOTFOUND', async () => {
@@ -103,13 +103,13 @@ describe('resolveMongodbSrv', () => {
103103
it('rejects when the TXT lookup returns more than one result', async () => {
104104
srvResult = [{ name: 'asdf.example.com', port: 27017 }];
105105
txtResult = [['a'], ['b']];
106-
assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
106+
await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
107107
});
108108

109109
it('rejects when the TXT lookup returns invalid connection string options', async () => {
110110
srvResult = [{ name: 'asdf.example.com', port: 27017 }];
111111
txtResult = [['a=b']];
112-
assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
112+
await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
113113
});
114114

115115
it('accepts TXT lookup authSource', async () => {
@@ -123,7 +123,7 @@ describe('resolveMongodbSrv', () => {
123123
it('rejects empty TXT lookup authSource', async () => {
124124
srvResult = [{ name: 'asdf.example.com', port: 27017 }];
125125
txtResult = [['authSource=']];
126-
assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
126+
await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
127127
});
128128

129129
it('prioritizes URL-provided over TXT lookup authSource', async () => {
@@ -145,7 +145,7 @@ describe('resolveMongodbSrv', () => {
145145
it('rejects empty TXT lookup replicaSet', async () => {
146146
srvResult = [{ name: 'asdf.example.com', port: 27017 }];
147147
txtResult = [['replicaSet=']];
148-
assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
148+
await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
149149
});
150150

151151
it('prioritizes URL-provided over TXT lookup replicaSet', async () => {
@@ -174,13 +174,13 @@ describe('resolveMongodbSrv', () => {
174174
it('rejects empty TXT lookup loadBalanced', async () => {
175175
srvResult = [{ name: 'asdf.example.com', port: 27017 }];
176176
txtResult = [['loadBalanced=']];
177-
assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
177+
await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
178178
});
179179

180180
it('rejects non true/false TXT lookup loadBalanced', async () => {
181181
srvResult = [{ name: 'asdf.example.com', port: 27017 }];
182182
txtResult = [['loadBalanced=bla']];
183-
assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
183+
await assert.rejects(resolveMongodbSrv('mongodb+srv://server.example.com', { dns }));
184184
});
185185

186186
it('prioritizes URL-provided over TXT lookup loadBalanced', async () => {
@@ -225,6 +225,40 @@ describe('resolveMongodbSrv', () => {
225225
await resolveMongodbSrv('mongodb+srv://server.example.com/?srvMaxHosts=1', { dns }),
226226
/^mongodb:\/\/host[1-3]\.example\.com\/\?tls=true$/);
227227
});
228+
229+
it('rejects SRV records without additional subdomain when parent domain has fewer than 3 parts', async () => {
230+
txtResult = [];
231+
srvResult = [{ name: 'example.com', port: 27017 }];
232+
await assert.rejects(resolveMongodbSrv('mongodb+srv://example.com', { dns }));
233+
});
234+
235+
it('not strip first subdomain when parent domain has fewer than 3 part to prevent TLD-only matching', async () => {
236+
txtResult = [];
237+
srvResult = [{ name: 'asdf.malicious.com', port: 27017 }];
238+
await assert.rejects(resolveMongodbSrv('mongodb+srv://example.com', { dns }));
239+
});
240+
241+
it('allow trailing dot in SRV lookup', async () => {
242+
txtResult = [];
243+
srvResult = [
244+
{ name: 'asdf.example.com', port: 27017 },
245+
{ name: 'meow.example.com', port: 27017 }
246+
];
247+
assert.strictEqual(
248+
await resolveMongodbSrv('mongodb+srv://server.example.com.', { dns }),
249+
'mongodb://asdf.example.com,meow.example.com/?tls=true');
250+
251+
srvResult = [
252+
{ name: 'asdf.example.com.', port: 27017 },
253+
{ name: 'meow.example.com', port: 27017 }
254+
];
255+
assert.strictEqual(
256+
await resolveMongodbSrv('mongodb+srv://server.example.com', { dns }),
257+
'mongodb://asdf.example.com.,meow.example.com/?tls=true');
258+
assert.strictEqual(
259+
await resolveMongodbSrv('mongodb+srv://server.example.com.', { dns }),
260+
'mongodb://asdf.example.com.,meow.example.com/?tls=true');
261+
});
228262
});
229263

230264
for (const [name, dnsProvider] of [

0 commit comments

Comments
 (0)