diff --git a/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.spec.tsx b/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.spec.tsx
index f47188cf3..79f398813 100644
--- a/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.spec.tsx
+++ b/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.spec.tsx
@@ -1,5 +1,6 @@
/* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */
import '@testing-library/jest-dom'
+import type { ElementType } from 'react'
import { readFileSync } from 'fs'
import { render, screen } from '@testing-library/react'
import remarkBreaks from 'remark-breaks'
@@ -9,6 +10,9 @@ import { MarkdownContent } from './MarkdownContent'
interface MarkdownRendererProps {
children: string
+ components: {
+ a: ElementType
+ }
remarkPlugins: unknown[]
skipHtml: boolean
}
@@ -65,6 +69,28 @@ describe('MarkdownContent', () => {
}))
})
+ it('opens GFM links in a safe new tab', () => {
+ const markdown = 'https://www.topcoder-dev.com/challenges'
+
+ render()
+ const markdownProps = mockReactMarkdown.mock.calls[0][0] as MarkdownRendererProps
+ const MarkdownLink = markdownProps.components.a
+
+ render(
+
+ {markdown}
+ ,
+ )
+ const link = screen.getByRole('link', { name: markdown })
+
+ expect(link)
+ .toHaveAttribute('href', markdown)
+ expect(link)
+ .toHaveAttribute('target', '_blank')
+ expect(link)
+ .toHaveAttribute('rel', 'noopener noreferrer')
+ })
+
it('keeps Markdown formatting visible after the platform style reset', () => {
expect(markdownStyles)
.toMatch(/a,[\s\S]*a:hover \{[\s\S]*color: \$link-blue-dark;[\s\S]*text-decoration: underline;/)
diff --git a/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.tsx b/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.tsx
index 8a0d1fb31..ce0c367d7 100644
--- a/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.tsx
+++ b/src/apps/support/src/lib/components/MarkdownContent/MarkdownContent.tsx
@@ -1,6 +1,6 @@
/** Safe Markdown renderer for user-authored support content. */
-import { FC } from 'react'
-import ReactMarkdown from 'react-markdown'
+import type { FC } from 'react'
+import ReactMarkdown, { type Components } from 'react-markdown'
import remarkBreaks from 'remark-breaks'
import remarkGfm from 'remark-gfm'
@@ -10,8 +10,28 @@ export interface MarkdownContentProps {
markdown: string
}
+const markdownComponents: Components = {
+ /**
+ * Renders a Markdown link in a new tab without exposing the opener page.
+ *
+ * @param props anchor attributes and AST metadata produced by ReactMarkdown.
+ * @returns a safely targeted anchor used for links in support conversations.
+ * @throws Does not throw.
+ */
+ a: props => (
+
+ {props.children}
+
+ ),
+}
+
/**
- * Renders GFM and line breaks while dropping raw HTML.
+ * Renders GFM and line breaks with links opening in new tabs while dropping raw HTML.
*
* @param props untrusted Markdown source.
* @returns safely rendered Markdown.
@@ -20,6 +40,7 @@ export interface MarkdownContentProps {
export const MarkdownContent: FC = props => (