Skip to content

Commit

Permalink
Improve link protection. Fix #80595
Browse files Browse the repository at this point in the history
  • Loading branch information
octref committed Sep 15, 2019
1 parent 77f14c3 commit 9d5086b
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 158 deletions.
164 changes: 24 additions & 140 deletions src/vs/workbench/contrib/url/common/trustedDomains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,177 +7,61 @@ import { localize } from 'vs/nls';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IProductService } from 'vs/platform/product/common/product';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { URI } from 'vs/base/common/uri';

export const enum ConfigureTrustedDomainActionType {
ToggleTrustAll = 'toggleTrustAll',
Add = 'add',
Configure = 'configure',
Reset = 'reset'
}
const TRUSTED_DOMAINS_URI = URI.parse('trustedDomains:/Trusted Domains');

export const configureTrustedDomainSettingsCommand = {
id: 'workbench.action.configureTrustedDomain',
description: {
description: localize('configureTrustedDomain', 'Configure Trusted Domains for Link Protection'),
description: localize('trustedDomain.configureTrustedDomain', 'Configure Trusted Domains'),
args: []
},
handler: async (accessor: ServicesAccessor) => {
const quickInputService = accessor.get(IQuickInputService);
const storageService = accessor.get(IStorageService);
const productService = accessor.get(IProductService);

let trustedDomains: string[] = productService.linkProtectionTrustedDomains
? [...productService.linkProtectionTrustedDomains]
: [];

try {
const trustedDomainsSrc = storageService.get('http.linkProtectionTrustedDomains', StorageScope.GLOBAL);
if (trustedDomainsSrc) {
trustedDomains = JSON.parse(trustedDomainsSrc);
}
} catch (err) { }

const trustOrUntrustAllLabel =
trustedDomains.indexOf('*') === -1
? localize('trustedDomain.trustAll', 'Disable Link Protection')
: localize('trustedDomain.untrustAll', 'Enable Link Protection');

const trustOrUntrustAll: IQuickPickItem = {
id: ConfigureTrustedDomainActionType.ToggleTrustAll,
label: trustOrUntrustAllLabel
};

const result = await quickInputService.pick(
[
trustOrUntrustAll,
{ id: ConfigureTrustedDomainActionType.Add, label: localize('trustedDomain.add', 'Add Trusted Domain') },
{
id: ConfigureTrustedDomainActionType.Configure,
label: localize('trustedDomain.edit', 'View and configure Trusted Domains')
},
{ id: ConfigureTrustedDomainActionType.Reset, label: localize('trustedDomain.reset', 'Reset Trusted Domains') }
],
{}
);

if (result) {
switch (result.id) {
case ConfigureTrustedDomainActionType.ToggleTrustAll:
toggleAll(trustedDomains, storageService);
break;
case ConfigureTrustedDomainActionType.Add:
addDomain(trustedDomains, storageService, quickInputService);
break;
case ConfigureTrustedDomainActionType.Configure:
configureDomains(trustedDomains, storageService, quickInputService);
break;
case ConfigureTrustedDomainActionType.Reset:
resetDomains(storageService, productService);
break;
}
}
const editorService = accessor.get(IEditorService);
editorService.openEditor({ resource: TRUSTED_DOMAINS_URI, mode: 'jsonc' });
return;
}
};

function toggleAll(trustedDomains: string[], storageService: IStorageService) {
if (trustedDomains.indexOf('*') === -1) {
storageService.store(
'http.linkProtectionTrustedDomains',
JSON.stringify(trustedDomains.concat(['*'])),
StorageScope.GLOBAL
);
} else {
storageService.store(
'http.linkProtectionTrustedDomains',
JSON.stringify(trustedDomains.filter(x => x !== '*')),
StorageScope.GLOBAL
);
}
}

function addDomain(trustedDomains: string[], storageService: IStorageService, quickInputService: IQuickInputService) {
quickInputService
.input({
placeHolder: 'Domain to trust',
validateInput: i => {
if (i.match(/^https?:\/\//)) {
return Promise.resolve(undefined);
}

return Promise.resolve(`${i} should start with http/https`);
}
})
.then(result => {
console.log(result);
if (result) {
storageService.store(
'http.linkProtectionTrustedDomains',
JSON.stringify(trustedDomains.concat([result])),
StorageScope.GLOBAL
);
}
});
}

function configureDomains(
trustedDomains: string[],
storageService: IStorageService,
quickInputService: IQuickInputService
) {
const domainQuickPickItems: IQuickPickItem[] = trustedDomains
.filter(d => d !== '*')
.map(d => {
return {
type: 'item',
label: d,
id: d,
picked: true
};
});

quickInputService.pick(domainQuickPickItems, { canPickMany: true }).then(result => {
const pickedDomains: string[] = result.map(r => r.id!);
storageService.store('http.linkProtectionTrustedDomains', JSON.stringify(pickedDomains), StorageScope.GLOBAL);
});
}

function resetDomains(storageService: IStorageService, productService: IProductService) {
if (productService.linkProtectionTrustedDomains) {
storageService.store(
'http.linkProtectionTrustedDomains',
JSON.stringify(productService.linkProtectionTrustedDomains),
StorageScope.GLOBAL
);
} else {
storageService.store('http.linkProtectionTrustedDomains', JSON.stringify([]), StorageScope.GLOBAL);
}
}

export async function configureOpenerTrustedDomainsHandler(
trustedDomains: string[],
domainToConfigure: string,
quickInputService: IQuickInputService,
storageService: IStorageService
storageService: IStorageService,
editorService: IEditorService
) {
const openAllLinksItem: IQuickPickItem = {
type: 'item',
label: localize('trustedDomain.trustAllAndOpenLink', 'Disable Link Protection and open link'),
id: '*',
picked: trustedDomains.indexOf('*') !== -1
};
const trustDomainItem: IQuickPickItem = {
const trustDomainAndOpenLinkItem: IQuickPickItem = {
type: 'item',
label: localize('trustedDomain.trustDomainAndOpenLink', 'Trust {0} and open link', domainToConfigure),
id: domainToConfigure,
picked: true
};
const configureTrustedDomainItem: IQuickPickItem = {
type: 'item',
label: localize('trustedDomain.configureTrustedDomains', 'Configure Trusted Domains'),
id: 'configure'
};

const pickedResult = await quickInputService.pick([openAllLinksItem, trustDomainItem], {
activeItem: trustDomainItem
const pickedResult = await quickInputService.pick([openAllLinksItem, trustDomainAndOpenLinkItem, configureTrustedDomainItem], {
activeItem: trustDomainAndOpenLinkItem
});

if (pickedResult) {
if (pickedResult.id === 'configure') {
editorService.openEditor({
resource: TRUSTED_DOMAINS_URI,
mode: 'jsonc'
});
return trustedDomains;
}
if (pickedResult.id && trustedDomains.indexOf(pickedResult.id) === -1) {
storageService.store(
'http.linkProtectionTrustedDomains',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Event } from 'vs/base/common/event';
import { parse } from 'vs/base/common/json';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import {
FileDeleteOptions,
FileOverwriteOptions,
FileSystemProviderCapabilities,
FileType,
FileWriteOptions,
IFileService,
IFileSystemProvider,
IStat,
IWatchOptions
} from 'vs/platform/files/common/files';
import { IProductService } from 'vs/platform/product/common/product';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';

const TRUSTED_DOMAINS_SCHEMA = 'trustedDomains';

const TRUSTED_DOMAINS_STAT: IStat = {
type: FileType.File,
ctime: Date.now(),
mtime: Date.now(),
size: 0
};

const CONFIG_HELP_TEXT = `// Configure trusted domains by editing and saving this file
// You can use \`*\` to match subdomains. For example https://*.visualstudio.com would match:
// - https://code.visualstudio.com
// - https://update.code.visualstudio.com
`;

export class TrustedDomainsFileSystemProvider implements IFileSystemProvider, IWorkbenchContribution {
readonly capabilities = FileSystemProviderCapabilities.FileReadWrite;

readonly onDidChangeCapabilities = Event.None;
readonly onDidChangeFile = Event.None;

constructor(
@IFileService private readonly fileService: IFileService,
@IProductService private readonly productService: IProductService,
@IStorageService private readonly storageService: IStorageService
) {
this.fileService.registerProvider(TRUSTED_DOMAINS_SCHEMA, this);
}

stat(resource: URI): Promise<IStat> {
return Promise.resolve(TRUSTED_DOMAINS_STAT);
}

readFile(resource: URI): Promise<Uint8Array> {
let trustedDomains: string[] = this.productService.linkProtectionTrustedDomains
? [...this.productService.linkProtectionTrustedDomains]
: [];

try {
const trustedDomainsSrc = this.storageService.get('http.linkProtectionTrustedDomains', StorageScope.GLOBAL);
if (trustedDomainsSrc) {
trustedDomains = JSON.parse(trustedDomainsSrc);
}
} catch (err) { }

const trustedDomainsContent = CONFIG_HELP_TEXT + JSON.stringify(trustedDomains, null, 2);

const buffer = Buffer.from(trustedDomainsContent, 'utf-8');
return Promise.resolve(buffer);
}

writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
let trustedDomainsd = [];

try {
trustedDomainsd = parse(content.toString());
} catch (err) { }

this.storageService.store(
'http.linkProtectionTrustedDomains',
JSON.stringify(trustedDomainsd),
StorageScope.GLOBAL
);

return Promise.resolve();
}

watch(resource: URI, opts: IWatchOptions): IDisposable {
return {
dispose() {
return;
}
};
}
mkdir(resource: URI): Promise<void> {
return Promise.resolve(undefined!);
}
readdir(resource: URI): Promise<[string, FileType][]> {
return Promise.resolve(undefined!);
}
delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
return Promise.resolve(undefined!);
}
rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
return Promise.resolve(undefined!);
}
}
35 changes: 21 additions & 14 deletions src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { configureOpenerTrustedDomainsHandler } from 'vs/workbench/contrib/url/common/trustedDomains';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';

export class OpenerValidatorContributions implements IWorkbenchContribution {
constructor(
@IOpenerService private readonly _openerService: IOpenerService,
@IStorageService private readonly _storageService: IStorageService,
@IDialogService private readonly _dialogService: IDialogService,
@IProductService private readonly _productService: IProductService,
@IQuickInputService private readonly _quickInputService: IQuickInputService
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@IEditorService private readonly _editorService: IEditorService
) {
this._openerService.registerValidator({ shouldOpen: r => this.validateLink(r) });
}
Expand Down Expand Up @@ -68,7 +70,8 @@ export class OpenerValidatorContributions implements IWorkbenchContribution {
trustedDomains,
domainToOpen,
this._quickInputService,
this._storageService
this._storageService,
this._editorService
);
// Trust all domains
if (pickedDomains.indexOf('*') !== -1) {
Expand Down Expand Up @@ -114,7 +117,7 @@ function isLocalhostAuthority(authority: string) {
*
* - Schemes must match
* - There's no subdomain matching. For example https://microsoft.com doesn't match https://www.microsoft.com
* - Star matches all. For example https://*.microsoft.com matches https://www.microsoft.com
* - Star matches all subdomains. For example https://*.microsoft.com matches https://www.microsoft.com and https://foo.bar.microsoft.com
*/
export function isURLDomainTrusted(url: URI, trustedDomains: string[]) {
if (isLocalhostAuthority(url.authority)) {
Expand All @@ -135,17 +138,21 @@ export function isURLDomainTrusted(url: URI, trustedDomains: string[]) {
if (trustedDomains[i].indexOf('*') !== -1) {
const parsedTrustedDomain = URI.parse(trustedDomains[i]);
if (url.scheme === parsedTrustedDomain.scheme) {
const authoritySegments = url.authority.split('.');
const trustedDomainAuthoritySegments = parsedTrustedDomain.authority.split('.');

if (authoritySegments.length === trustedDomainAuthoritySegments.length) {
if (
authoritySegments.every(
(val, i) => trustedDomainAuthoritySegments[i] === '*' || val === trustedDomainAuthoritySegments[i]
)
) {
return true;
}
let reversedAuthoritySegments = url.authority.split('.').reverse();
const reversedTrustedDomainAuthoritySegments = parsedTrustedDomain.authority.split('.').reverse();
if (
reversedTrustedDomainAuthoritySegments.length < reversedAuthoritySegments.length &&
reversedTrustedDomainAuthoritySegments[reversedTrustedDomainAuthoritySegments.length - 1] === '*'
) {
reversedAuthoritySegments = reversedAuthoritySegments.slice(0, reversedTrustedDomainAuthoritySegments.length);
}

if (
reversedAuthoritySegments.every((val, i) => {
return reversedTrustedDomainAuthoritySegments[i] === '*' || val === reversedTrustedDomainAuthoritySegments[i];
})
) {
return true;
}
}
}
Expand Down
Loading

0 comments on commit 9d5086b

Please sign in to comment.