diff --git a/web/src/components/console/utils/link.spec.tsx b/web/src/components/console/utils/link.spec.tsx new file mode 100644 index 000000000..ec4d141d4 --- /dev/null +++ b/web/src/components/console/utils/link.spec.tsx @@ -0,0 +1,45 @@ +import { renderToStaticMarkup } from 'react-dom/server'; + +jest.mock('@patternfly/react-core', () => ({ + Button: ({ children, href, rel, target }) => ( + + {children} + + ), + Icon: ({ children }) => <>{children}, +})); + +jest.mock('@patternfly/react-icons', () => ({ + ExternalLinkAltIcon: () => null, +})); + +jest.mock('react-linkify', () => ({ children }) => <>{children}); + +import { ExternalLink } from './link'; + +describe('ExternalLink', () => { + it.each([ + 'https://runbooks.example.com/alert', + 'http://runbooks.example.com/alert?severity=high', + ])('renders an absolute HTTP(S) URL as a link: %s', (href) => { + const html = renderToStaticMarkup(); + + expect(html).toContain(`href="${href}"`); + }); + + it.each([ + 'javascript:alert(1)', + 'JaVaScRiPt:alert(1)', + 'data:text/html,', + 'vbscript:msgbox(1)', + 'mailto:security@example.com', + '/runbooks/alert', + '//runbooks.example.com/alert', + '\tjavascript:alert(1)', + 'not a URL', + ])('renders an unsafe or invalid URL as text: %s', (href) => { + const html = renderToStaticMarkup(); + + expect(html).not.toMatch(/]/); + }); +}); diff --git a/web/src/components/console/utils/link.tsx b/web/src/components/console/utils/link.tsx index 4df925b70..16830abb3 100644 --- a/web/src/components/console/utils/link.tsx +++ b/web/src/components/console/utils/link.tsx @@ -1,36 +1,42 @@ -import type { FC, ReactNode } from 'react'; +import type { FC, PropsWithChildren, ReactNode } from 'react'; import Linkify from 'react-linkify'; import { Button, Icon } from '@patternfly/react-core'; import { ExternalLinkAltIcon } from '@patternfly/react-icons'; -export const ExternalLink: FC = ({ +export const ExternalLink: FC> = ({ children, href, text, additionalClassName = '', dataTestID, stopPropagation, -}) => ( - -); +}) => { + if (!isSafeExternalURL(href)) { + return <>{children || text}; + } + + return ( + + ); +}; // Open links in a new window and set noopener/noreferrer. export const LinkifyExternal: FC<{ children: ReactNode }> = ({ children }) => ( @@ -45,3 +51,12 @@ type ExternalLinkProps = { dataTestID?: string; stopPropagation?: boolean; }; + +const isSafeExternalURL = (value: string): value is string => { + if (!URL.canParse(value)) { + return false; + } + + const { protocol } = new URL(value); + return protocol === 'http:' || protocol === 'https:'; +};