diff --git a/frontend/src/shared/ui/Markdown/Markdown.test.tsx b/frontend/src/shared/ui/Markdown/Markdown.test.tsx index 8eab72e..06d7d86 100644 --- a/frontend/src/shared/ui/Markdown/Markdown.test.tsx +++ b/frontend/src/shared/ui/Markdown/Markdown.test.tsx @@ -36,6 +36,30 @@ describe('Markdown', () => { expect((window as unknown as { __pwned?: boolean }).__pwned).toBeUndefined() }) + // 위 케이스는 react-markdown 이 raw HTML 을 escape 하므로 rehype-sanitize 가 없어도 통과한다. + // 반면 링크 protocol 필터링은 **오직 rehype-sanitize 만** 막아준다 — 즉 이 케이스가 + // sanitize 플러그인이 실제로 붙어 동작하는지 검증하는 유일한 지점이다. + // 렌더 대상(분석 문서 마크다운·answerRewrite)은 사용자 이력서·답변에서 파생되므로, + // 스키마를 커스터마이즈하거나 플러그인을 빼는 변경이 조용히 통과하면 안 된다. + it.each([ + ['javascript:', '[클릭](javascript:alert(1))'], + ['대소문자 우회', '[클릭](JaVaScRiPt:alert(1))'], + ['data: HTML', '[클릭](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)'], + ])('%s 링크는 href 가 제거된다', async (_label, markdown) => { + const { container } = render({markdown}) + + await screen.findByText('클릭') + // 링크 텍스트는 남기되 이동 가능한 href 는 남기지 않는다. + expect(container.querySelector('a')?.getAttribute('href') ?? null).toBeNull() + }) + + it('일반 http(s) 링크는 그대로 살린다 — 필터가 과하게 걷어내지 않는지', async () => { + const { container } = render({'[문서](https://example.com/a)'}) + + await screen.findByText('문서') + expect(container.querySelector('a')).toHaveAttribute('href', 'https://example.com/a') + }) + it('코드 블록은 pre 로 감싸 가로 스크롤 컨테이너에 렌더된다', async () => { const { container } = render( {'```java\n@Transactional\npublic void end() {}\n```'},