瀏覽代碼

Neo: allow only http(s) URLs in inline post embeds (#5787)

Inline embed URLs are now parsed and must be http: or https:; anything
else is dropped. The fallback shown before/without UNA's embed answer
is rendered with the Link component instead of hand-built raw HTML.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
RomanL 1 周之前
父節點
當前提交
5f328f079d
共有 2 個文件被更改,包括 26 次插入 和 5 次删除
  1. 15 2
      neo/packages/app/lib/editor/inline-video.ts
  2. 11 3
      neo/packages/app/ui/molecules/content/inline-embed.js

+ 15 - 2
neo/packages/app/lib/editor/inline-video.ts

@@ -64,6 +64,19 @@ export function toVideoPosterSrc(mp4Link: string) {
 
 
 const decodeAmp = (s: string) => s.replace(/&amp;/g, '&')
 const decodeAmp = (s: string) => s.replace(/&amp;/g, '&')
 
 
+/** Only http(s) URLs may be embedded: anything else (`javascript:`, `data:`…) is dropped. */
+export function isHttpUrl(url: string | null | undefined): boolean {
+    if (!url) return false
+    try {
+        const { protocol } = new URL(url)
+        return protocol === 'http:' || protocol === 'https:'
+    } catch {
+        return false
+    }
+}
+
+const embedSegment = (url: string): InlineSegment | null => (isHttpUrl(url) ? { type: 'embed', url } : null)
+
 const IMG = '<img\\b[^>]*\\bsrc="([^"]+)"[^>]*>'
 const IMG = '<img\\b[^>]*\\bsrc="([^"]+)"[^>]*>'
 // Legacy (link) format of the first builds: a paragraph with only a marked link.
 // Legacy (link) format of the first builds: a paragraph with only a marked link.
 const LINK = '<a\\b[^>]*\\bhref="([^"]+)"[^>]*>[^<]*</a>'
 const LINK = '<a\\b[^>]*\\bhref="([^"]+)"[^>]*>[^<]*</a>'
@@ -82,14 +95,14 @@ function toSegment(rawSrc: string | undefined, isLink: boolean): InlineSegment |
     const fragment = src.slice(hash + 1)
     const fragment = src.slice(hash + 1)
     if (!isLink && fragment.startsWith(EMBED_MARK)) {
     if (!isLink && fragment.startsWith(EMBED_MARK)) {
         try {
         try {
-            return { type: 'embed', url: decodeURIComponent(fragment.slice(EMBED_MARK.length)) }
+            return embedSegment(decodeURIComponent(fragment.slice(EMBED_MARK.length)))
         } catch {
         } catch {
             return null
             return null
         }
         }
     }
     }
     // Legacy embed link: `URL#neo-embed` / `URL#frag&neo-embed`.
     // Legacy embed link: `URL#neo-embed` / `URL#frag&neo-embed`.
     if (isLink && /(^|&)neo-embed$/.test(fragment)) {
     if (isLink && /(^|&)neo-embed$/.test(fragment)) {
-        return { type: 'embed', url: src.replace(/[#&]neo-embed$/, '') }
+        return embedSegment(src.replace(/[#&]neo-embed$/, ''))
     }
     }
     return null
     return null
 }
 }

+ 11 - 3
neo/packages/app/ui/molecules/content/inline-embed.js

@@ -1,17 +1,25 @@
 import { useFetch } from 'app/lib/hooks/use-fetch'
 import { useFetch } from 'app/lib/hooks/use-fetch'
 import { appSetting } from 'app/lib/util'
 import { appSetting } from 'app/lib/util'
+import { isHttpUrl } from 'app/lib/editor/inline-video'
 import { View } from 'app/design/view'
 import { View } from 'app/design/view'
-import Html from 'app/ui/atoms/html'
+import Link from 'app/ui/atoms/link'
 import Embed from 'app/ui/molecules/content/embed'
 import Embed from 'app/ui/molecules/content/embed'
 
 
 /** Embed-button link in a post body: UNA embed card / player (same API as the `embed` form field). */
 /** Embed-button link in a post body: UNA embed card / player (same API as the `embed` form field). */
 export default function InlineEmbed({ url }) {
 export default function InlineEmbed({ url }) {
+    const safe = isHttpUrl(url)
     // Encoded: the URL is a query value (`params[]=`), its own `&` would split it.
     // Encoded: the URL is a query value (`params[]=`), its own `&` would split it.
-    const { data: response } = useFetch(`/api.php?r=${appSetting('urls', 'embeds_new')}${encodeURIComponent(url)}`)
+    const { data: response } = useFetch(safe ? `/api.php?r=${appSetting('urls', 'embeds_new')}${encodeURIComponent(url)}` : null)
+
+    if (!safe) return null
 
 
     // Until UNA answers (or when it can't embed the URL) keep the plain link.
     // Until UNA answers (or when it can't embed the URL) keep the plain link.
     if (!response?.data)
     if (!response?.data)
-        return <Html data={`<p><a href="${url.replace(/"/g, '&quot;')}">${url.replace(/</g, '&lt;')}</a></p>`} />
+        return (
+            <View className='my-3'>
+                <Link href={url} asExternal>{url}</Link>
+            </View>
+        )
 
 
     return (
     return (
         <View className='w-full my-3'>
         <View className='w-full my-3'>