Skip to content

Commit

Permalink
feat: disable certificate validation for localhost (microsoft#332)
Browse files Browse the repository at this point in the history
* feat: disable certificate validation for localhost

Also improves the `isLoopback` check to do a DNS lookup to handle any
hosts configuration or exotic addresses.

Fixes microsoft#309

* fixup! tests and pr comment
  • Loading branch information
connor4312 authored Feb 13, 2020
1 parent f438a94 commit 5736338
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 26 deletions.
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"execa": "^4.0.0",
"glob-stream": "^6.1.0",
"inversify": "^5.0.1",
"ip": "^1.1.5",
"js-beautify": "^1.10.0",
"jsonc-parser": "^2.2.0",
"micromatch": "^4.0.2",
Expand Down Expand Up @@ -82,6 +83,7 @@
"@types/glob": "^7.1.1",
"@types/glob-stream": "^6.1.0",
"@types/http-server": "^0.10.0",
"@types/ip": "^1.1.0",
"@types/js-beautify": "^1.8.1",
"@types/json-schema": "^7.0.3",
"@types/lodash": "^4.14.144",
Expand Down
8 changes: 6 additions & 2 deletions src/common/objUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,9 @@ export function once<T>(fn: () => T): () => T {
/**
* Memoizes the single-parameter function.
*/
export function memoize<T, R>(fn: (arg: T) => R): (arg: T) => R {
export function memoize<T, R>(fn: (arg: T) => R): ((arg: T) => R) & { clear(): void } {
const cached = new Map<T, R>();
return arg => {
const wrapper = (arg: T): R => {
if (cached.has(arg)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return cached.get(arg)!;
Expand All @@ -205,6 +205,10 @@ export function memoize<T, R>(fn: (arg: T) => R): (arg: T) => R {
cached.set(arg, value);
return value;
};

wrapper.clear = () => cached.clear();

return wrapper;
}

/**
Expand Down
79 changes: 57 additions & 22 deletions src/common/urlUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import * as https from 'https';
import { fixDriveLetterAndSlashes } from './pathUtils';
import { AnyChromiumConfiguration } from '../configuration';
import { escapeRegexSpecialChars } from './stringUtils';
import { promises as dns } from 'dns';
import { memoize } from './objUtils';
import ipModule from 'ip';

let isCaseSensitive = process.platform !== 'win32';

Expand Down Expand Up @@ -56,6 +59,56 @@ export const nearestDirectoryWhere = async (
}
};

const localv4 = ipModule.toBuffer('127.0.0.1');
const localv6 = ipModule.toBuffer('::1');

/**
* Checks if the given address, well-formed loopback IPs. We don't need exotic
* variations like `127.1` because `dns.lookup()` will resolve the proper
* version for us. The "right" way would be to parse the IP to an integer
* like Go does (https://golang.org/pkg/net/#IP.IsLoopback), but this
* is lightweight and works.
*/
const isLoopbackIp = (ipOrLocalhost: string) => {
if (ipOrLocalhost.toLowerCase() === 'localhost') {
return true;
}

let buf: Buffer;
try {
buf = ipModule.toBuffer(ipOrLocalhost);
} catch {
return false;
}

return buf.equals(localv4) || buf.equals(localv6);
};

/**
* Gets whether the IP is a loopback address.
*/
export const isLoopback = memoize(async (address: string) => {
let ipOrHostname: string;
try {
const url = new URL(address);
// replace brackets in ipv6 addresses:
ipOrHostname = url.hostname.replace(/^\[|\]$/g, '');
} catch {
ipOrHostname = address;
}

if (isLoopbackIp(ipOrHostname)) {
return true;
}

try {
const resolved = await dns.lookup(ipOrHostname);
return isLoopbackIp(resolved.address);
} catch {
return false;
}
});

export async function fetch(url: string): Promise<string> {
if (url.startsWith('data:')) {
const prefix = url.substring(0, url.indexOf(','));
Expand All @@ -79,9 +132,11 @@ export async function fetch(url: string): Promise<string> {
});
}

const driver = url.startsWith('https://') ? https : http;
const isSecure = !url.startsWith('http://');
const driver = isSecure ? https : http;
const validateCerts = isSecure && !(await isLoopback(url));
return new Promise<string>((fulfill, reject) => {
const request = driver.get(url, response => {
const request = driver.get(url, { rejectUnauthorized: !validateCerts }, response => {
let data = '';
response.setEncoding('utf8');
response.on('data', (chunk: string) => (data += chunk));
Expand Down Expand Up @@ -294,26 +349,6 @@ export function platformPathToPreferredCase(p: string | undefined): string | und
return p;
}

const loopbacks: ReadonlySet<string> = new Set([
'localhost',
'127.0.0.1',
'::1',
'0:0:0:0:0:0:0:1',
]);

/**
* Returns whether the given URL is a loopback address.
*/

export const isLoopback = (address: string) => {
try {
const url = new URL(address);
return loopbacks.has(url.hostname);
} catch {
return loopbacks.has(address);
}
};

/**
* Creates a target filter function for the given Chrome configuration.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/targets/node/nodeAttacher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export class NodeAttacher extends NodeAttacherBase<INodeAttachConfiguration> {
return;
}

if (!isLoopback(run.params.address)) {
if (!(await isLoopback(run.params.address))) {
this.logger.warn(LogTag.RuntimeTarget, 'Cannot attach to children of remote process');
return;
}
Expand Down
49 changes: 48 additions & 1 deletion src/test/common/urlUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { expect } from 'chai';
import { stub } from 'sinon';
import { SinonStub, stub } from 'sinon';
import { promises as dns } from 'dns';
import * as os from 'os';
import {
fileUrlToAbsolutePath,
createTargetFilter,
urlToRegex,
setCaseSensitivePaths,
resetCaseSensitivePaths,
isLoopback,
} from '../../common/urlUtils';

describe('urlUtils', () => {
Expand Down Expand Up @@ -201,4 +203,49 @@ describe('urlUtils', () => {
]);
});
});

describe('isLoopback', () => {
let lookupStub: SinonStub;

beforeEach(() => {
lookupStub = stub(dns, 'lookup');
lookupStub.callThrough();
lookupStub.withArgs('contoso.com').resolves({ address: '1.1.1.1' });
lookupStub.withArgs('local.contoso.com').resolves({ address: '127.0.0.1' });
});

afterEach(() => {
isLoopback.clear();
lookupStub.restore();
});

const ttable = {
'127.0.0.1': true,
'http://127.1/foo': true,
'http://1.1.1.1/foo': false,
'totes invalid': false,
'1.1.1.1': false,
'::1': true,
':0:1': true,
'0:0:0:0:0:0:0:1': true,
':1:1': false,
'http://[::1]/foo': true,
'http://[:1:1]/foo': false,

'http://contoso.com/foo': false,
'http://local.contoso.com/foo': true,
};

// Alternative forms supported by posix:
if (process.platform !== 'win32') {
Object.assign(ttable, {
'127.1': true,
'0x7f000001': true,
});
}

for (const [ip, expected] of Object.entries(ttable)) {
it(ip, async () => expect(await isLoopback(ip)).to.equal(expected));
}
});
});

0 comments on commit 5736338

Please sign in to comment.