Code change:
return true; } <del> const domain = `${url.scheme}://${url.authority}`; </del> for (let i = 0; i < trustedDomains.length; i++) { if (trustedDomains[i] === '*') { return true; } <del> if (trustedDomains[i] === domain) { </del> <ins> if (isTrusted(url.toString(), trustedDomains[i])) { </ins> return true; } <ins> } </ins> <del> let parsedTrustedDomain; if (/^https?:///.test(trustedDomains[i])) { parsedTrustedDomain = URI.parse(trustedDomains[i]); if (url.scheme !== parsedTrustedDomain.scheme) { continue; } } else { parsedTrustedDomain = URI.parse('https://' + trustedDomains[i]); } </del> <ins> return false; } </ins> <del> if (url.authority === parsedTrustedDomain.authority) { if (pathMatches(url.path, parsedTrustedDomain.path)) { return true; } else { continue; } } </del> <ins> export const isTrusted = (url: string, trustedURL: string): boolean => { const normalize = (url: string) => url.replace(//+$/, ''); trustedURL = normalize(trustedURL); url = normalize(url); </ins> <del> if (trustedDomains[i].indexOf('*') !== -1) { </del> <ins> const memo = Array.from({ length: url.length + 1 }).map(() => Array.from({ length: trustedURL.length + 1 }).map(() => undefined), ); </ins> <del> let reversedAuthoritySegments = url.authority.split('.').reverse(); const reversedTrustedDomainAuthoritySegments = parsedTrustedDomain.authority.split('.').reverse(); </del> <ins> if (/^[^./:]*:///.test(trustedURL)) { return doURLMatch(memo, url, trustedURL, 0, 0); } </ins> <del> if ( reversedTrustedDomainAuthoritySegments.length < reversedAuthoritySegments.length && reversedTrustedDomainAuthoritySegments[reversedTrustedDomainAuthoritySegments.length - 1] === '*' ) { reversedAuthoritySegments = reversedAuthoritySegments.slice(0, reversedTrustedDomainAuthoritySegments.length); } </del> <ins> const scheme = /^(https?):///.exec(url)?.[1]; if (scheme) { return doURLMatch(memo, url, `${scheme}://${trustedURL}`, 0, 0); } </ins> <del> const authorityMatches = reversedAuthoritySegments.every((val, i) => { return reversedTrustedDomainAuthoritySegments[i] === '*' || val === reversedTrustedDomainAuthoritySegments[i]; }); </del> <ins> return false; }; </ins> <del> if (authorityMatches && pathMatches(url.path, parsedTrustedDomain.path)) { return true; } } </del> <ins> const doURLMatch = ( memo: (boolean | undefined)[][], url: string, trustedURL: string, urlOffset: number, trustedURLOffset: number, ): boolean => { if (memo[urlOffset]?.[trustedURLOffset] !== undefined) { return memo[urlOffset][trustedURLOffset]!; </ins> } <del> return false; } </del> <ins> const options = []; </ins> <del> function pathMatches(open: string, rule: string) { if (rule === '/') { return true; </del> <ins> // Endgame. // Fully exact match if (urlOffset === url.length) { return trustedURLOffset === trustedURL.length; </ins> } <del> if (rule[rule.length - 1] === '/') { rule = rule.slice(0, -1); </del> <ins> // Some path remaining in url if (trustedURLOffset === trustedURL.length) { const remaining = url.slice(urlOffset); return remaining[0] === '/'; </ins> } <del> const openSegments = open.split('/'); const ruleSegments = rule.split('/'); for (let i = 0; i < ruleSegments.length; i++) { if (ruleSegments[i] !== openSegments[i]) { return false; </del> <ins> if (url[urlOffset] === trustedURL[trustedURLOffset]) { // Exact match. options.push(doURLMatch(memo, url, trustedURL, urlOffset + 1, trustedURLOffset + 1)); } if (trustedURL[trustedURLOffset] + trustedURL[trustedURLOffset + 1] === '*.') { // Any subdomain match. Either consume one thing that's not a / or : and don't advance base or consume nothing and do. if (!['/', ':'].includes(url[urlOffset])) { options.push(doURLMatch(memo, url, trustedURL, urlOffset + 1, trustedURLOffset)); </ins> } <ins> options.push(doURLMatch(memo, url, trustedURL, urlOffset, trustedURLOffset + 2)); </ins> } <del> return true; } </del> <ins> if (trustedURL[trustedURLOffset] + trustedURL[trustedURLOffset + 1] === '.*' && url[urlOffset] === '.') { // IP mode. Consume one segment of numbers or nothing. let endBlockIndex = urlOffset + 1; do { endBlockIndex++; } while (/[0-9]/.test(url[endBlockIndex])); if (['.', ':', '/', undefined].includes(url[endBlockIndex])) { options.push(doURLMatch(memo, url, trustedURL, endBlockIndex, trustedURLOffset + 2)); } } if (trustedURL[trustedURLOffset] + trustedURL[trustedURLOffset + 1] === ':*') { // any port match. Consume a port if it exists otherwise nothing. Always comsume the base. if (url[urlOffset] === ':') { let endPortIndex = urlOffset + 1; do { endPortIndex++; } while (/[0-9]/.test(url[endPortIndex])); options.push(doURLMatch(memo, url, trustedURL, endPortIndex, trustedURLOffset + 2)); } else { options.push(doURLMatch(memo, url, trustedURL, urlOffset, trustedURLOffset + 2)); } } return (memo[urlOffset][trustedURLOffset] = options.some(a => a === true)); }; </ins>

Related discussion:
text: Issue Type: <bBug</bWrite http://192.168.1.7:3000/ into a text file. Command-click it to try to open the link. You will get an alert asking whether you want to open the external website. Click “Configure Trusted Domains”. <img width="1025" alt="link and dialog asking to confirm opening external website" src="https://user-"Look at the second option in the menu. It says “Trust 1.7:3000 and all its subdomains”. But 1.7:3000 is not a valid address. <img width="1024" alt="link and menu with buggy entry “Trust 1.7:3000 and all its subdomains”" src="https://user-"I can only guess that VS Code thinks that “7” is a TLD and “1.7” is a full domain name. What I actually want to do is whitelist opening all links to IP addresses like . In other words, white-list the subnet . At least, I think that makes sense. But I am not a network engineer, so if it makes more sense to recommend to the user whitelisting some other subnet such as , or whitelisting only the address but on any port, that would be fine too. VS Code version: Code 1.40.1 (, 2019-11-13T16:47:44.719Z) OS version: Darwin x64 18.7.0 <!-- generated by issue reporter --