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
27 changes: 19 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,24 @@

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

function matchesParentDomain (srvAddress: string, parentDomain: string): boolean {
const regex = /^.*?\./;
const srv = `.${srvAddress.replace(regex, '')}`;
const parent = `.${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<Options['dns']>, lookupAddress: string, srvServiceName: string): Promise<string[]> {
Expand All @@ -27,9 +40,7 @@
}

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}`));
Expand All @@ -39,7 +50,7 @@
let records: string[][] | undefined;
try {
records = await promisify(dns.resolveTxt)(lookupAddress);
} catch (err: any) {

Check warning on line 53 in src/index.ts

View workflow job for this annotation

GitHub Actions / Test (macos-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 53 in src/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 12.x)

Unexpected any. Specify a different type

Check warning on line 53 in src/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 14.x)

Unexpected any. Specify a different type

Check warning on line 53 in src/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 14.x)

Unexpected any. Specify a different type

Check warning on line 53 in src/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 53 in src/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 53 in src/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 12.x)

Unexpected any. Specify a different type
if (err?.code && (err.code !== 'ENODATA' && err.code !== 'ENOTFOUND')) {
throw err;
}
Expand Down
58 changes: 46 additions & 12 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@
describe('resolveMongodbSrv', () => {
context('with a fake resolver', () => {
let srvError: Error | null;
let srvResult: any[];

Check warning on line 9 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (macos-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 9 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 12.x)

Unexpected any. Specify a different type

Check warning on line 9 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 14.x)

Unexpected any. Specify a different type

Check warning on line 9 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 14.x)

Unexpected any. Specify a different type

Check warning on line 9 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 9 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 9 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 12.x)

Unexpected any. Specify a different type
let txtError: Error | null;
let txtResult: string[][];
let srvQueries: string[];
let txtQueries: string[];

const dns = {
resolveSrv (hostname: string, cb: any): void {

Check warning on line 16 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (macos-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 16 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 12.x)

Unexpected any. Specify a different type

Check warning on line 16 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 14.x)

Unexpected any. Specify a different type

Check warning on line 16 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 14.x)

Unexpected any. Specify a different type

Check warning on line 16 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 16 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 16 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 12.x)

Unexpected any. Specify a different type
srvQueries.push(hostname);
process.nextTick(cb, srvError, srvResult);
},
resolveTxt (hostname: string, cb: any): void {

Check warning on line 20 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (macos-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 20 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 12.x)

Unexpected any. Specify a different type

Check warning on line 20 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 14.x)

Unexpected any. Specify a different type

Check warning on line 20 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 14.x)

Unexpected any. Specify a different type

Check warning on line 20 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 20 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 20 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 12.x)

Unexpected any. Specify a different type
txtQueries.push(hostname);
process.nextTick(cb, txtError, txtResult);
}
Expand Down Expand Up @@ -49,26 +49,26 @@
});

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 () => {
Expand All @@ -81,7 +81,7 @@
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 () => {
Expand All @@ -103,13 +103,13 @@
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 () => {
Expand All @@ -123,7 +123,7 @@
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 () => {
Expand All @@ -145,7 +145,7 @@
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 () => {
Expand Down Expand Up @@ -174,13 +174,13 @@
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 () => {
Expand Down Expand Up @@ -225,6 +225,40 @@
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('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 = [
{ 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 [
Expand All @@ -236,7 +270,7 @@
this.timeout(30_000);

it('works', async () => {
const str = await resolveMongodbSrv('mongodb+srv://user:password@cluster0.ucdwm.mongodb.net/', { dns: dnsProvider } as any);

Check warning on line 273 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (macos-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 273 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 12.x)

Unexpected any. Specify a different type

Check warning on line 273 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 14.x)

Unexpected any. Specify a different type

Check warning on line 273 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 14.x)

Unexpected any. Specify a different type

Check warning on line 273 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 273 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 16.x)

Unexpected any. Specify a different type

Check warning on line 273 in test/index.ts

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 12.x)

Unexpected any. Specify a different type
assert([
'mongodb://user:password@cluster0-shard-00-00.ucdwm.mongodb.net,cluster0-shard-00-01.ucdwm.mongodb.net,cluster0-shard-00-02.ucdwm.mongodb.net/?authSource=admin&replicaSet=atlas-jt9dqp-shard-0&tls=true',
'mongodb://user:password@cluster0-shard-00-00.ucdwm.mongodb.net,cluster0-shard-00-02.ucdwm.mongodb.net,cluster0-shard-00-01.ucdwm.mongodb.net/?authSource=admin&replicaSet=atlas-jt9dqp-shard-0&tls=true',
Expand Down
Loading