diff --git a/src/vs/workbench/contrib/url/common/trustedDomainsFileSystemProvider.ts b/src/vs/workbench/contrib/url/common/trustedDomainsFileSystemProvider.ts index f38fa6ed5836d49252d32b6af2501cdd35cdcebc..763f038e087c82b1c9c4cf7882697b9ca18fdeb2 100644 --- a/src/vs/workbench/contrib/url/common/trustedDomainsFileSystemProvider.ts +++ b/src/vs/workbench/contrib/url/common/trustedDomainsFileSystemProvider.ts @@ -26,6 +26,8 @@ const TRUSTED_DOMAINS_STAT: IStat = { const CONFIG_HELP_TEXT_PRE = `// Links matching one or more entries in the list below can be opened without link protection. // The following examples show what entries can look like: // - "https://microsoft.com": Matches this specific domain using https +// - "https://microsoft.com/foo": Matches https://microsoft.com/foo and https://microsoft.com/foo/bar, +// but not https://microsoft.com/foobar or https://microsoft.com/bar // - "https://*.microsoft.com": Match all domains ending in "microsoft.com" using https // - "microsoft.com": Match this specific domain using either http or https // - "*.microsoft.com": Match all domains ending in "microsoft.com" using either http or https diff --git a/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts b/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts index ebe36feb710de6b1eda1a935d7ce535defeb14a4..afef62fa5e110c00178da79f82586042fc0478bd 100644 --- a/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts +++ b/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts @@ -5,7 +5,7 @@ import { Schemas } from 'vs/base/common/network'; import Severity from 'vs/base/common/severity'; -import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { equalsIgnoreCase, startsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -14,7 +14,10 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { configureOpenerTrustedDomainsHandler, readTrustedDomains } from 'vs/workbench/contrib/url/common/trustedDomains'; +import { + configureOpenerTrustedDomainsHandler, + readTrustedDomains +} from 'vs/workbench/contrib/url/common/trustedDomains'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class OpenerValidatorContributions implements IWorkbenchContribution { @@ -132,10 +135,11 @@ export function isURLDomainTrusted(url: URI, trustedDomains: string[]) { } if (url.authority === parsedTrustedDomain.authority) { - return true; + return pathMatches(url.path, parsedTrustedDomain.path); } if (trustedDomains[i].indexOf('*') !== -1) { + let reversedAuthoritySegments = url.authority.split('.').reverse(); const reversedTrustedDomainAuthoritySegments = parsedTrustedDomain.authority.split('.').reverse(); @@ -146,11 +150,11 @@ export function isURLDomainTrusted(url: URI, trustedDomains: string[]) { reversedAuthoritySegments = reversedAuthoritySegments.slice(0, reversedTrustedDomainAuthoritySegments.length); } - if ( - reversedAuthoritySegments.every((val, i) => { - return reversedTrustedDomainAuthoritySegments[i] === '*' || val === reversedTrustedDomainAuthoritySegments[i]; - }) - ) { + const authorityMatches = reversedAuthoritySegments.every((val, i) => { + return reversedTrustedDomainAuthoritySegments[i] === '*' || val === reversedTrustedDomainAuthoritySegments[i]; + }); + + if (authorityMatches && pathMatches(url.path, parsedTrustedDomain.path)) { return true; } } @@ -158,3 +162,19 @@ export function isURLDomainTrusted(url: URI, trustedDomains: string[]) { return false; } + +function pathMatches(open: string, rule: string) { + if (rule === '/') { + return true; + } + + const openSegments = open.split('/'); + const ruleSegments = rule.split('/'); + for (let i = 0; i < ruleSegments.length; i++) { + if (ruleSegments[i] !== openSegments[i]) { + return false; + } + } + + return true; +} diff --git a/src/vs/workbench/test/contrib/linkProtection.test.ts b/src/vs/workbench/test/contrib/linkProtection.test.ts index f027009a14131dc323481db3e6d55c75d456d6f0..43c443119a7d7d06b5e30e7266c08e4f911c29ef 100644 --- a/src/vs/workbench/test/contrib/linkProtection.test.ts +++ b/src/vs/workbench/test/contrib/linkProtection.test.ts @@ -8,49 +8,67 @@ import * as assert from 'assert'; import { isURLDomainTrusted } from 'vs/workbench/contrib/url/common/trustedDomainsValidator'; import { URI } from 'vs/base/common/uri'; -function linkProtectedByRules(link: string, rules: string[]) { - assert.ok(isURLDomainTrusted(URI.parse(link), rules)); +function linkAllowedByRules(link: string, rules: string[]) { + assert.ok(isURLDomainTrusted(URI.parse(link), rules), `Link\n${link}\n should be protected by rules\n${JSON.stringify(rules)}`); } -function linkNotProtectedByRules(link: string, rules: string[]) { - assert.ok(!isURLDomainTrusted(URI.parse(link), rules)); +function linkNotAllowedByRules(link: string, rules: string[]) { + assert.ok(!isURLDomainTrusted(URI.parse(link), rules), `Link\n${link}\n should NOT be protected by rules\n${JSON.stringify(rules)}`); } suite('Link protection domain matching', () => { test('simple', () => { - linkNotProtectedByRules('https://x.org', []); + linkNotAllowedByRules('https://x.org', []); - linkProtectedByRules('https://x.org', ['https://x.org']); - linkProtectedByRules('https://x.org/foo', ['https://x.org']); + linkAllowedByRules('https://x.org', ['https://x.org']); + linkAllowedByRules('https://x.org/foo', ['https://x.org']); - linkNotProtectedByRules('https://x.org', ['http://x.org']); - linkNotProtectedByRules('http://x.org', ['https://x.org']); + linkNotAllowedByRules('https://x.org', ['http://x.org']); + linkNotAllowedByRules('http://x.org', ['https://x.org']); - linkNotProtectedByRules('https://www.x.org', ['https://x.org']); + linkNotAllowedByRules('https://www.x.org', ['https://x.org']); - linkProtectedByRules('https://www.x.org', ['https://www.x.org', 'https://y.org']); + linkAllowedByRules('https://www.x.org', ['https://www.x.org', 'https://y.org']); }); test('localhost', () => { - linkProtectedByRules('https://127.0.0.1', []); - linkProtectedByRules('https://127.0.0.1:3000', []); - linkProtectedByRules('https://localhost', []); - linkProtectedByRules('https://localhost:3000', []); + linkAllowedByRules('https://127.0.0.1', []); + linkAllowedByRules('https://127.0.0.1:3000', []); + linkAllowedByRules('https://localhost', []); + linkAllowedByRules('https://localhost:3000', []); }); test('* star', () => { - linkProtectedByRules('https://a.x.org', ['https://*.x.org']); - linkProtectedByRules('https://a.b.x.org', ['https://*.x.org']); - linkProtectedByRules('https://a.x.org', ['https://a.x.*']); - linkProtectedByRules('https://a.x.org', ['https://a.*.org']); - linkProtectedByRules('https://a.x.org', ['https://*.*.org']); - linkProtectedByRules('https://a.b.x.org', ['https://*.b.*.org']); - linkProtectedByRules('https://a.a.b.x.org', ['https://*.b.*.org']); + linkAllowedByRules('https://a.x.org', ['https://*.x.org']); + linkAllowedByRules('https://a.b.x.org', ['https://*.x.org']); + linkAllowedByRules('https://a.x.org', ['https://a.x.*']); + linkAllowedByRules('https://a.x.org', ['https://a.*.org']); + linkAllowedByRules('https://a.x.org', ['https://*.*.org']); + linkAllowedByRules('https://a.b.x.org', ['https://*.b.*.org']); + linkAllowedByRules('https://a.a.b.x.org', ['https://*.b.*.org']); }); test('no scheme', () => { - linkProtectedByRules('https://a.x.org', ['a.x.org']); - linkProtectedByRules('https://a.x.org', ['*.x.org']); - linkProtectedByRules('https://a.b.x.org', ['*.x.org']); - linkProtectedByRules('https://x.org', ['*.x.org']); + linkAllowedByRules('https://a.x.org', ['a.x.org']); + linkAllowedByRules('https://a.x.org', ['*.x.org']); + linkAllowedByRules('https://a.b.x.org', ['*.x.org']); + linkAllowedByRules('https://x.org', ['*.x.org']); + }); + + test('sub paths', () => { + linkAllowedByRules('https://x.org/foo', ['https://x.org/foo']); + linkAllowedByRules('https://x.org/foo', ['x.org/foo']); + linkAllowedByRules('https://x.org/foo', ['*.org/foo']); + + linkNotAllowedByRules('https://x.org/bar', ['https://x.org/foo']); + linkNotAllowedByRules('https://x.org/bar', ['x.org/foo']); + linkNotAllowedByRules('https://x.org/bar', ['*.org/foo']); + + linkAllowedByRules('https://x.org/foo/bar', ['https://x.org/foo']); + linkNotAllowedByRules('https://x.org/foo2', ['https://x.org/foo']); + + linkNotAllowedByRules('https://www.x.org/foo', ['https://x.org/foo']); + + linkNotAllowedByRules('https://a.x.org/bar', ['https://*.x.org/foo']); + linkNotAllowedByRules('https://a.b.x.org/bar', ['https://*.x.org/foo']); }); });