XSS 학습 노트
PayloadsAllTheThings, OWASP, MDN 자료를 바탕으로 이 플레이그라운드가 다루는 XSS 공격면과 방어 체크리스트를 실무 관점으로 정리했습니다.
테스트 방법
- 먼저 입력이 어느 컨텍스트에 출력되는지 확인합니다: HTML text, attribute, URL, JavaScript string, CSS, DOM sink, iframe wrapper.
- 그 다음 컨텍스트별 payload 를 하나씩 붙여 넣고, 실행 여부뿐 아니라 어떤 정책이 막았는지 기록합니다.
- 사용자 화면만 보지 말고 관리자 콘솔, 알림, 메일, 로그 뷰어, 미리보기, 검색 인덱서처럼 나중에 렌더링되는 화면까지 같은 payload 로 확인합니다.
놓치기 쉬운 공격면
script 태그를 지워도 onerror, autofocus/onfocus, SVG onload, 속성 따옴표 탈출이 남을 수 있습니다.
href, src, action, data 속성은 javascript:, data:text/html, 인코딩된 프로토콜을 정규화 후 allowlist 해야 합니다.
location, hash, postMessage, storage 값을 innerHTML, insertAdjacentHTML, eval 류 API 로 넘기면 서버 로그 없이 실행됩니다.
ProseMirror, Markdown, MDX 렌더러는 저장 형식과 최종 HTML 이 달라집니다. 최종 렌더 HTML 기준으로 sanitize 해야 합니다.
embed 기능은 태그 허용만으로 끝나지 않습니다. URL host/protocol allowlist, sandbox, allow 속성, referrer 정책을 함께 봐야 합니다.
SVG/HTML 업로드와 관리자 검수 화면은 나중에 실행되는 저장형 XSS 의 대표적인 지점입니다.
방어 체크리스트
- 출력 컨텍스트별 인코딩을 적용하고, HTML 이 필요한 곳만 검증된 sanitizer 를 사용합니다.
- URL 은 디코딩/정규화 후 protocol + host allowlist 로 검증합니다.
- DOM sink 는 textContent, setAttribute, createElement 같은 구조적 API 로 대체합니다.
- DOMPurify 를 쓴다면 허용 태그/속성/URL policy 를 제품 기능 단위로 문서화하고 테스트합니다.
- iframe 은 필요한 host 만 허용하고 sandbox 기본값을 보수적으로 잡습니다.
- CSP 는 보조 안전망으로 두되, sanitize 실패를 대신하는 단일 방어선으로 보지 않습니다.
프로젝트에서 토론하고 싶은 실제 고민
ProseMirror 로 저장한 글을 별도 renderer 로 보여주면서 XSS surface 가 생겼고, DOMPurify 를 적용했습니다. 문제는 embed 기능입니다. 유튜브, 코드펜, 문서, 지도처럼 사용자가 원하는 링크가 다양할 때 iframe URL 을 host allowlist 로만 관리하는 것이 현실적인지 계속 토론이 필요합니다.
프로필 필드는 작아서 놓치기 쉽습니다. 닉네임이 속성으로 들어가거나 아이콘 URL 이 커스텀 renderer 를 거치면 댓글, 알림, 관리자 목록 전체로 저장형 XSS 가 퍼질 수 있습니다.