62. XSS 헌팅: 기초부터 버그 바운티까지
XSS를 찾는 일은 페이로드를 많이 아는 일이 아니라 입력이 어떤 문맥에서 다시 해석되는지 추적하는 일이다. 아래 예시는 로컬 랩, CTF, 또는 명시적으로 허가된 테스트 대상에서만 실행한다. 쿠키 탈취, 키로깅, 계정 작업, 제3자 콜백은 실습에서 사용하지 않는다.
소스에서 싱크까지
먼저 검색·프로필·댓글·파일명·에러 페이지 등 입력의 소스를 찾고, HTML 텍스트, 속성, URL, JavaScript 문자열, CSS 중 어디로 들어가는지 표시한다. 같은 값이라도 HTML 텍스트와 href 속성은 필요한 인코딩이 다르다. innerHTML, document.write, eval, 문자열을 받는 setTimeout, 위험한 URL 대입은 싱크 후보지만, 호출 주변의 검증과 Trusted Types 정책까지 읽어야 결론을 낼 수 있다.
안전한 존재 확인은 실행 코드 대신 고유한 무해 문자열을 쓰고, 필요할 때만 브라우저의 개발자 도구에서 DOM이 텍스트인지 마크업인지 확인하는 것이다.
const output = document.querySelector("#result");
output.textContent = new URLSearchParams(location.search).get("q") ?? "";
반대로 다음은 입력을 HTML로 재해석한다.
output.innerHTML = new URLSearchParams(location.search).get("q") ?? "";
유형보다 문맥
Reflected XSS는 한 응답에서 반사되고, Stored XSS는 저장 후 다른 사용자의 화면에서 재현되며, DOM XSS는 서버 응답보다 클라이언트 코드의 데이터 흐름이 핵심이다. 유형을 먼저 외우면 놓치는 것은 문맥이다. 출력 위치를 확인한 뒤 해당 문맥에 맞는 인코더를 적용하고, HTML을 허용해야 한다면 검증된 sanitizer를 사용한다.
브라우저에서 <script>가 보이지 않는다고 안전한 것도 아니다. HTML 엔티티로 인코딩된 텍스트는 보통 실행되지 않지만, 이 값이 다시 innerHTML이나 위험한 URL로 흘러가면 재해석될 수 있다. 필터 우회 목록을 수집하기보다 “검증 전후에 문맥이 바뀌는가”를 재현한다.
승인된 검증 절차
- 테스트 계정과 고유한 marker를 만든다.
- 입력이 저장되는 지점과 읽히는 지점을 기록한다.
- 응답 원문, DOM, 프레임, CSP를 각각 확인한다.
- 실행 여부가 필요하면 본인 브라우저에서 무해한
alert대신 콘솔 marker를 사용한다. - 실제 사용자 데이터에 접근하지 않고 즉시 삭제한다.
Burp Suite는 요청과 응답의 문맥을 비교하는 데 유용하고, DevTools의 Elements/Sources 탭은 DOM sink와 CSP 위반을 확인하는 데 유용하다. 블라인드 XSS를 검증해야 할 경우에도 외부 수집 서버로 문서나 쿠키를 보내지 말고, 소유한 테스트 엔드포인트에 최소 marker만 기록한다.
방어는 층이 아니라 문맥의 일치다
템플릿의 기본 자동 이스케이핑을 유지하고, HTML·속성·JavaScript·URL별 출력 인코딩을 사용한다. HTML을 허용하는 기능은 허용 태그와 속성을 명시한 sanitizer로 제한한다. CSP는 피해를 줄이는 보조선이지 잘못된 출력 처리를 대신하지 않는다. 세션 쿠키의 HttpOnly, Secure, SameSite 설정도 필요하지만 DOM XSS의 실행 자체를 고치지는 않는다.
코드 리뷰에서는 사용자 입력이 “한 번 안전해졌다”는 주장을 믿지 말고, 문자열 결합과 sink 사이의 모든 변환을 확인한다. 수정 후에는 marker가 텍스트로 남는지, 허용된 서식은 유지되는지, CSP 리포트와 회귀 테스트가 있는지를 검증한다. XSS 헌팅의 좋은 결과는 화려한 우회가 아니라 데이터 흐름과 영향 범위를 재현 가능한 한 문장으로 설명하는 것이다.