[Write-Up] PortSwigger - Stored DOM XSS
목차
문제 분석
난이도:
PRACTITIONER
Lab: Stored DOM XSS
댓글 데이터는 서버에 저장된 뒤 JavaScript를 통해 DOM에 렌더링된다. 클라이언트가 적용하는 불완전한 HTML 이스케이프를 우회해 alert()를 호출하면 문제가 해결된다.
XSS 진단
댓글을 불러오는 loadCommentsWithVulnerableEscapeHtml.js에서 다음 로직을 확인할 수 있다.
function escapeHTML(html) {
return html.replace('<', '<').replace('>', '>');
}
function displayComments(comments) {
/* 생략 */
if (comment.body) {
let commentBodyPElement = document.createElement('p');
commentBodyPElement.innerHTML = escapeHTML(comment.body);
commentSection.appendChild(commentBodyPElement);
}
}
댓글 본문은 escapeHTML()을 거친 뒤 innerHTML에 대입된다. 하지만 문자열을 첫 번째 인자로 받은 replace()는 처음 발견한 문자 하나만 치환한다. 여러 개의 <, >를 입력하면 첫 번째 쌍 뒤에 남는 문자는 그대로 HTML 파서에 전달된다.
익스플로잇
첫 번째 꺾쇠가 이스케이프에 소비되도록 다음 댓글을 저장한다.
<<img onerror=alert(1) src='>'>
첫 <와 첫 >만 각각 <, >로 바뀌고, 남은 <img ...> 구조는 유효한 요소로 파싱된다. innerHTML로 삽입한 <script>는 실행되지 않으므로 로드 오류 이벤트를 사용할 수 있는 img 요소를 선택했다.

게시글을 조회하면 잘못된 이미지 주소가 onerror를 발생시키고 alert()가 호출되어 문제가 해결된다.

정리
Stored DOM XSS는 저장된 데이터가 클라이언트 코드의 위험한 sink를 거치면서 발생한다. 이 랩의 방어는 치환 대상 전체가 아니라 첫 문자만 처리해 쉽게 우회된다. 부분적인 문자열 치환 대신 안전한 DOM API로 텍스트를 삽입하거나 검증된 HTML sanitizer를 사용해야 한다.
