화면이 그려지는 5단계

  1. 요청: 브라우저가 서버에 페이지 요청
  2. 응답: 서버가 HTML을 글자 덩어리로 반환, 이 시점엔 그냥 긴 텍스트
  3. 파싱: 글자를 위에서 아래로 한 줄씩 읽으며 DOM 구성 → <script> 만나면 즉시 실행
  4. 완료: 끝까지 읽으면 DOM 완성(파서 종료)
  5. 수정: 이후부터 완성된 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 엔티티 (&#40; 등): 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 파서가 읽는 자리라 &#40;가 풀리지만, <script> 안은 JS 엔진 담당이라 안 풀림