diff --git a/frontend/src/lib/components/MarkdownDoc.svelte b/frontend/src/lib/components/MarkdownDoc.svelte index be83685..47d44a2 100644 --- a/frontend/src/lib/components/MarkdownDoc.svelte +++ b/frontend/src/lib/components/MarkdownDoc.svelte @@ -160,6 +160,24 @@ ph.dataset.mdImageSwapped = '1'; } }); + + // 외부 http(s) 링크 → 새 탭(target=_blank) + 보안 rel(noopener noreferrer). + // marked 렌더러/DOMPurify 를 건드리지 않고 sanitize 후 라이브 DOM 에 속성을 부여한다 — + // heading anchor 와 동일한 DOM 후처리 패턴(전역 DOMPurify hook 오염·렌더러 API 의존 없이 안전). + // 앵커(#)·상대경로·mailto 등 비-http(s) 링크는 손대지 않는다(SPA 내부 항법 보존). + $effect(() => { + void renderedHtml; + if (!containerRef) return; + const links = containerRef.querySelectorAll('a[href]'); + for (const a of links) { + if (a.dataset.extlinkProcessed === '1') continue; + const href = a.getAttribute('href') ?? ''; + if (!/^https?:\/\//i.test(href)) continue; // 외부 http(s) 만 + a.setAttribute('target', '_blank'); + a.setAttribute('rel', 'noopener noreferrer'); + a.dataset.extlinkProcessed = '1'; + } + });