From f78a4c8aee818220595f679453113e13f1ab460e Mon Sep 17 00:00:00 2001 From: xuzijie1995 <18852951350@163.com> Date: Wed, 4 Jun 2025 18:15:30 +0800 Subject: [PATCH] fix(markdown): Ensure abbr: links render correctly in react-markdown v9+ React-markdown v9 and later versions enforce stricter default URL filtering, which inadvertently removed support for custom URL schemes like 'abbr:'. This commit introduces a `customUrlTransform` function, now located in `markdown-utils.ts`. This function is passed to the `ReactMarkdown` component to: - Explicitly allow the 'abbr:' protocol. - Permit standard safe web protocols (http, https, mailto, xmpp, irc, ircs). - Allow page-local fragments (#), protocol-relative URLs (//), and all purely relative paths. - Disallow other potentially unsafe or unsupported URL schemes. This restores the intended functionality for 'abbr:' links while maintaining robust URL handling and security for the Markdown component. --- web/app/components/base/markdown/index.tsx | 3 +- .../base/markdown/markdown-utils.ts | 54 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/web/app/components/base/markdown/index.tsx b/web/app/components/base/markdown/index.tsx index 0e0dc41cf2..28fb73653d 100644 --- a/web/app/components/base/markdown/index.tsx +++ b/web/app/components/base/markdown/index.tsx @@ -7,7 +7,7 @@ import RemarkGfm from 'remark-gfm' import RehypeRaw from 'rehype-raw' import { flow } from 'lodash-es' import cn from '@/utils/classnames' -import { preprocessLaTeX, preprocessThinkTag } from './markdown-utils' +import { preprocessLaTeX, preprocessThinkTag, customUrlTransform } from './markdown-utils' import { AudioBlock, CodeBlock, @@ -65,6 +65,7 @@ export function Markdown(props: { content: string; className?: string; customDis } }, ]} + urlTransform={customUrlTransform} disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]} components={{ code: CodeBlock, diff --git a/web/app/components/base/markdown/markdown-utils.ts b/web/app/components/base/markdown/markdown-utils.ts index d77b2ddccf..9fa7164b65 100644 --- a/web/app/components/base/markdown/markdown-utils.ts +++ b/web/app/components/base/markdown/markdown-utils.ts @@ -36,3 +36,57 @@ export const preprocessThinkTag = (content: string) => { (str: string) => str.replace(/(<\/details>)(?![^\S\r\n]*[\r\n])(?![^\S\r\n]*$)/g, '$1\n'), ])(content) } + +/** + * Transforms a URI for use in react-markdown, ensuring security and compatibility. + * This function is designed to work with react-markdown v9+ which has stricter + * default URL handling. + * + * Behavior: + * 1. Always allows the custom 'abbr:' protocol. + * 2. Always allows page-local fragments (e.g., "#some-id"). + * 3. Always allows protocol-relative URLs (e.g., "//example.com/path"). + * 4. Always allows purely relative paths (e.g., "path/to/file", "/abs/path"). + * 5. Allows absolute URLs if their scheme is in a permitted list (case-insensitive): + * 'http:', 'https:', 'mailto:', 'xmpp:', 'irc:', 'ircs:'. + * 6. Intelligently distinguishes colons used for schemes from colons within + * paths, query parameters, or fragments of relative-like URLs. + * 7. Returns the original URI if allowed, otherwise returns `undefined` to + * signal that the URI should be removed/disallowed by react-markdown. + */ +export const customUrlTransform = (uri: string): string | undefined => { + const PERMITTED_SCHEME_REGEX = /^(https?|ircs?|mailto|xmpp|abbr):$/i; + + if (uri.startsWith('#')) { + return uri; + } + + if (uri.startsWith('//')) { + return uri; + } + + const colonIndex = uri.indexOf(':'); + + if (colonIndex === -1) { + return uri; + } + + const slashIndex = uri.indexOf('/'); + const questionMarkIndex = uri.indexOf('?'); + const hashIndex = uri.indexOf('#'); + + if ( + (slashIndex !== -1 && colonIndex > slashIndex) || + (questionMarkIndex !== -1 && colonIndex > questionMarkIndex) || + (hashIndex !== -1 && colonIndex > hashIndex) + ) { + return uri; + } + + const scheme = uri.substring(0, colonIndex + 1).toLowerCase(); + if (PERMITTED_SCHEME_REGEX.test(scheme)) { + return uri; + } + + return undefined; +} \ No newline at end of file