diff --git a/react-app/src/shared/utils/externalHref.test.ts b/react-app/src/shared/utils/externalHref.test.ts index d60146d6d..4ca666efa 100644 --- a/react-app/src/shared/utils/externalHref.test.ts +++ b/react-app/src/shared/utils/externalHref.test.ts @@ -6,6 +6,7 @@ describe('externalHref', () => { it('returns http(s) URLs unchanged (Angular binds item.url as-is)', () => { expect(externalHref('https://example.com/a?b=1')).toBe('https://example.com/a?b=1'); expect(externalHref('http://example.com')).toBe('http://example.com'); + expect(externalHref('httpS://example.com/a')).toBe('httpS://example.com/a'); }); it.each([ @@ -13,6 +14,8 @@ describe('externalHref', () => { ['javascript: URL', 'javascript:alert(1)'], ['data: URL', 'data:text/html,hi'], ['"http" prefix that is not a URL', 'httpfoo'], + ['scheme-only https URL', 'https:example.com'], + ['scheme-only http URL with one slash', 'http:/example.com'], ['empty string', ''], ['undefined', undefined], ])('returns null for %s', (_, url) => { diff --git a/react-app/src/shared/utils/externalHref.ts b/react-app/src/shared/utils/externalHref.ts index f08101d0e..384865e9c 100644 --- a/react-app/src/shared/utils/externalHref.ts +++ b/react-app/src/shared/utils/externalHref.ts @@ -1,11 +1,15 @@ import { isSafeHref } from './sanitizeHtml'; /** - * Angular `hasUrl` (`url.indexOf('http') === 0`), tightened: only a parseable, safe http(s) URL renders as an - * external link. Everything else (HN-relative `item?id=N` for Ask/Show/text posts) links to `/item/:id`. + * Angular `hasUrl` (`url.indexOf('http') === 0`), tightened: only a parseable, safe, absolute `http(s)://` URL renders + * as an external link. Everything else (HN-relative `item?id=N` for Ask/Show/text posts) links to `/item/:id`. + * Scheme-only forms like `https:example.com` are rejected: `new URL` accepts them, but as an href the browser resolves + * them relative to the page. */ +const ABSOLUTE_HTTP = /^http[sS]?:\/\//; + export function externalHref(url: string | null | undefined): string | null { - if (typeof url !== 'string' || url.indexOf('http') !== 0 || !isSafeHref(url)) return null; + if (typeof url !== 'string' || !ABSOLUTE_HTTP.test(url) || !isSafeHref(url)) return null; try { const parsed = new URL(url); return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? url : null;