화면이 그려지는 5단계
- 요청: 브라우저가 서버에 페이지 요청
- 응답: 서버가 HTML을 글자 덩어리로 반환, 이 시점엔 그냥 긴 텍스트
- 파싱: 글자를 위에서 아래로 한 줄씩 읽으며 DOM 구성 →
<script>만나면 즉시 실행 - 완료: 끝까지 읽으면 DOM 완성(파서 종료)
- 수정: 이후부터 완성된 DOM을 JS가 고치는 단계
핵심: <script> 실행 조건은 3단계(파서가 읽는 중)에 그 <script>가 글자 속에 존재 → 4단계 이후 뒤늦게 추가된 <script>는 파서가 이미 손을 뗐으므로 실행 안 됨
서버 쪽 (1→2단계 사이)
요청(1)을 받은 서버가 응답(2) 글자를 만드는 과정:
- 요청에서 파라미터 추출:
param = request.args.get(...) - 추출값 필터·검증:
xss_filter등 서버 측 처리 - 템플릿에 끼워 최종 HTML 완성 → 이게 2단계 글자 덩어리
서버 측 필터는 전부 여기서 끝남 → 3단계 파싱 전. 브라우저가 글자를 읽을 시점이면 페이로드는 이미 박혀있거나(통과) 지워진 상태(차단)
주의 — “파싱” 두 가지:
- 서버의
param = request: 요청에서 값 추출(쿼리스트링 파싱) - 3단계 파싱: 서버가 보낸 HTML 글자 → DOM
- 같은 단어, 다른 작업. 필터가 도는 곳은 전자(서버), DOM 생성은 후자(브라우저)
그래서 아래 “서버 직접 출력"에서 <script>가 2단계 글자에 포함되는 건 서버 필터를 통과했다는 의미
디코딩이 일어나는 위치
같은 인코딩이라도 어느 단계에서 풀리느냐가 다름 → 우회 가능 여부가 여기서 갈림
URL 인코딩 (%3c 등): 요청 수신 직후, 필터보다 먼저 풀림
- 필터는 풀린 상태를 검사 → 전송용 포장일 뿐, 우회력 없음
- 즉
%28로 보내도 필터가 볼 땐(
HTML 엔티티 (( 등): 3단계 HTML 파서가 읽을 때 풀림
- 단, 푸는 주체가 HTML 파서라서 자리를 탐
- 본문·속성값 자리 → HTML 파서 담당 → 풀림
<script>안 → JS 엔진 담당 → HTML 엔티티 모름 → 안 풀림
핵심: 인코딩이 통하려면 “필터를 속이고” + “도착 자리에서 풀려야” 함. URL 인코딩은 필터 전에 풀려 첫 조건 실패. HTML 엔티티는 코드 자리(<script>)에선 둘째 조건 실패
sink별 결론
같은 ?param=<script>alert(1)</script> 입력 시:
- 서버 직접 출력 (
return param):<script>가 2단계 응답 글자에 이미 포함 → 3단계에서 파서가 만남 → 실행 document.write(param): 3단계 도중<script>를 글자에 삽입 → 아직 파서가 읽는 중 → 실행innerHTML = param: 5단계에서 추가 → 파서 종료 후 → 태그는 DOM에 생성되나 실행 안 됨
innerHTML이 <script>를 막는 것은 보안 규칙(HTML5 명세) → 사용자 입력이 innerHTML에 들어가는 순간 코드 실행으로 뚫리는 것을 막기 위해, 나중에 삽입된 script의 실행을 차단(이벤트 핸들러는 동작 가능)
그래서 innerHTML sink에서 <script>가 막히면, 파서를 거치지 않고 JS를 발화시키는 이벤트 핸들러(<img onerror> 등)로 우회
인코딩 우회도 같은 원리: 이벤트 핸들러 속성은 HTML 파서가 읽는 자리라 (가 풀리지만, <script> 안은 JS 엔진 담당이라 안 풀림