From 5e0a6a12ff4f9b3398f3ffeb24fc3b06bab846aa Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 19 Mar 2024 12:01:54 -0700 Subject: [PATCH] Update trusted hosts, allow `#`, and add more tests (#3232) * Update trusted hosts, allow `#`, and add more tests * update comments --- __tests__/lib/strings/url-helpers.test.ts | 38 +++++++++++++++++++++++ src/lib/strings/url-helpers.ts | 38 ++++++++++++++++------- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/__tests__/lib/strings/url-helpers.test.ts b/__tests__/lib/strings/url-helpers.test.ts index 6ac31aeb63..fb4b8f7555 100644 --- a/__tests__/lib/strings/url-helpers.test.ts +++ b/__tests__/lib/strings/url-helpers.test.ts @@ -4,6 +4,7 @@ import { linkRequiresWarning, isPossiblyAUrl, splitApexDomain, + isTrustedUrl, } from '../../../src/lib/strings/url-helpers' describe('linkRequiresWarning', () => { @@ -74,6 +75,10 @@ describe('linkRequiresWarning', () => { // bad uri inputs, default to true ['', '', true], ['example.com', 'example.com', true], + ['/profile', 'Username', false], + ['#', 'Show More', false], + ['https://docs.bsky.app', 'https://docs.bsky.app', false], + ['https://bsky.app/compose/intent?text=test', 'Compose a post', false], ] it.each(cases)( @@ -139,3 +144,36 @@ describe('splitApexDomain', () => { }, ) }) + +describe('isTrustedUrl', () => { + const cases = [ + ['#', true], + ['#profile', true], + ['/', true], + ['/profile', true], + ['/profile/', true], + ['/profile/bob.test', true], + ['https://bsky.app', true], + ['https://bsky.app/', true], + ['https://bsky.app/profile/bob.test', true], + ['https://www.bsky.app', true], + ['https://www.bsky.app/', true], + ['https://docs.bsky.app', true], + ['https://bsky.social', true], + ['https://bsky.social/blog', true], + ['https://blueskyweb.xyz', true], + ['https://blueskyweb.zendesk.com', true], + ['http://bsky.app', true], + ['http://bsky.social', true], + ['http://blueskyweb.xyz', true], + ['http://blueskyweb.zendesk.com', true], + ['https://google.com', false], + ['https://docs.google.com', false], + ['https://google.com/#', false], + ] + + it.each(cases)('given input uri %p, returns %p', (str, expected) => { + const output = isTrustedUrl(str) + expect(output).toEqual(expected) + }) +}) diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index 820311e4ee..70a2b70692 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -4,6 +4,23 @@ import TLDs from 'tlds' import psl from 'psl' export const BSKY_APP_HOST = 'https://bsky.app' +const BSKY_TRUSTED_HOSTS = [ + 'bsky.app', + 'bsky.social', + 'blueskyweb.xyz', + 'blueskyweb.zendesk.com', + ...(__DEV__ ? ['localhost:19006', 'localhost:8100'] : []), +] + +/* + * This will allow any BSKY_TRUSTED_HOSTS value by itself or with a subdomain. + * It will also allow relative paths like /profile as well as #. + */ +const TRUSTED_REGEX = new RegExp( + `^(http(s)?://(([\\w-]+\\.)?${BSKY_TRUSTED_HOSTS.join( + '|([\\w-]+\\.)?', + )})|/|#)`, +) export function isValidDomain(str: string): boolean { return !!TLDs.find(tld => { @@ -86,6 +103,10 @@ export function isExternalUrl(url: string): boolean { return external || rss } +export function isTrustedUrl(url: string): boolean { + return TRUSTED_REGEX.test(url) +} + export function isBskyPostUrl(url: string): boolean { if (isBskyAppUrl(url)) { try { @@ -163,8 +184,8 @@ export function feedUriToHref(url: string): string { export function linkRequiresWarning(uri: string, label: string) { const labelDomain = labelToDomain(label) - // If the uri started with a / we know it is internal. - if (isRelativeUrl(uri)) { + // We should trust any relative URL or a # since we know it links to internal content + if (isRelativeUrl(uri) || uri === '#') { return false } @@ -176,18 +197,11 @@ export function linkRequiresWarning(uri: string, label: string) { } const host = urip.hostname.toLowerCase() - // Hosts that end with bsky.app or bsky.social should be trusted by default. - if ( - host.endsWith('bsky.app') || - host.endsWith('bsky.social') || - host.endsWith('blueskyweb.xyz') - ) { - // if this is a link to internal content, - // warn if it represents itself as a URL to another app + if (isTrustedUrl(uri)) { + // if this is a link to internal content, warn if it represents itself as a URL to another app return !!labelDomain && labelDomain !== host && isPossiblyAUrl(labelDomain) } else { - // if this is a link to external content, - // warn if the label doesnt match the target + // if this is a link to external content, warn if the label doesnt match the target if (!labelDomain) { return true }