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:';
+};