포스트

XSS 분류 — Reflected·Stored·DOM-based와 Source/Sink

세 XSS의 갈림길은 '악성 스크립트가 어디서 페이지에 삽입되는가'다. 서버 응답이면 Reflected, 저장 후 전달이면 Stored, 클라이언트 JS의 DOM 조작이면 DOM-based. View Source로 구분하는 법과, DOM-based를 관통하는 Source→Sink 흐름을 정리한다.

XSS 분류 — Reflected·Stored·DOM-based와 Source/Sink

XSS를 Reflected/Stored/DOM-based로 나누긴 하는데, 셋의 진짜 갈림길은 악성 스크립트가 어디서 페이지에 삽입되는가다. 삽입 주체가 서버냐 클라이언트 JS냐를 잡으면 분류도 방어 위치도 따라온다.

세 유형

 삽입되는 곳취약점 원인
Reflected(반사형)요청값이 서버 응답 HTML에 되돌아 실림서버 측 출력 처리 미흡
Stored(저장형)페이로드가 DB 등에 저장됐다가 나중에 전달저장·출력 시 미흡
DOM-based서버 무관, 클라이언트 JS가 DOM 조작 중 실행클라이언트 JS의 DOM 처리 미흡

쉽게 말해 Reflected는 “서버가 되돌려준 응답”에서 터지고, DOM-based는 “브라우저 안 JS”에서 터진다. Stored는 페이로드가 서버에 저장됐다 재전달된다는 점에서 또 다르다.

Reflected vs DOM-based — View Source로 구분

가장 헷갈리는 두 유형이다. 결정적 판별은 “페이지 소스 보기(View Source)에 페이로드가 보이느냐”다.

Reflected — 페이로드가 요청(URL 쿼리스트링)에 담겨 서버까지 갔다가 응답 HTML에 삽입돼 되돌아온다.

1
2
공격 URL:  https://site/search?q=<script>steal()</script>
서버 응답: <p>검색 결과: <script>steal()</script></p>   ← 응답에 그대로 실림

→ 서버 응답에 페이로드가 있으니 View Source에 보인다. 서버 응답 검사로 탐지 가능.

DOM-based — 서버는 페이로드를 본 적도 없다. 클라이언트 JS가 location.hash 같은 값을 읽어 DOM에 집어넣는다.

1
2
3
4
5
6
<!-- 서버가 보낸 건 멀쩡한 HTML -->
<div id="o"></div>
<script>
  // URL의 # 뒤 값을 읽어 그대로 DOM에 삽입
  document.getElementById("o").innerHTML = location.hash.substring(1);
</script>

서버 응답엔 페이로드가 없다. View Source엔 안 보이고 개발자도구의 DOM을 봐야 보인다. DOM 동적 분석이 필요.

 ReflectedDOM-based
페이로드가 서버까지 감O△ (fragment면 X)
응답 HTML에 삽입O (서버가)X (JS가)
View Source에 페이로드보임안 보임
탐지서버 응답 검사DOM 동적 분석

주의: DOM-based가 꼭 #fragment만 쓰는 건 아니다. ?q=...(서버로 가는 값)를 클라이언트 JS가 읽어 처리하는 DOM-based도 있다. 이땐 페이로드가 서버로도 가지만 서버는 정상 처리하고, 실제 취약점은 클라이언트 JS의 DOM 조작에서 발생한다. 진짜 기준은 “어느 코드가 페이로드를 페이지에 넣어 실행시키는가” — 서버 출력이면 Reflected, 클라이언트 JS면 DOM-based다.

방어 위치가 다르다

결함이 생기는 위치가 다르니 막는 위치도 다르다.

  • Reflected/Stored서버 측 출력 인코딩/이스케이프로 막는다. 응답에 실을 때 <, >, & 등을 엔티티로 바꾼다.
  • DOM-based클라이언트 JS에서 안전한 API 사용으로 막는다. innerHTML 대신 textContent, eval·document.write 회피.

DOM-based를 관통하는 것 — Source → Sink

DOM-based XSS는 결국 “공격자가 통제 가능한 값(source)이 검증 없이 위험한 지점(sink)으로 흘러간다”는 하나의 구조다.

  • Source(소스) — 공격자가 통제할 수 있는 입력을 JS가 읽어오는 지점. location.hash, location.search, document.referrer, window.name 등.
  • Sink(싱크) — 그 값이 흘러 들어가 실행으로 이어지는 위험한 지점. innerHTML, outerHTML, eval, document.write, setTimeout(문자열) 등.
1
2
let x = location.hash.substring(1);           // ← source (공격자 통제)
document.getElementById("o").innerHTML = x;   // ← sink (실행)

막는 법은 둘 중 하나다.

  • 위험한 sink를 안전한 것으로 교체 — innerHTMLtextContent
  • source 값을 sink에 넣기 전 인코딩·검증

“JS로 DOM을 통제한다”는 그 통제 과정에 외부 입력이 섞여드는 게 취약점이다. source와 sink만 추적하면 DOM-based는 대부분 잡힌다.

정리

  • 세 XSS의 갈림길은 삽입 주체 — 서버 응답(Reflected) / 저장 후 전달(Stored) / 클라이언트 JS의 DOM 조작(DOM-based).
  • Reflected vs DOM-based는 View Source에 페이로드가 보이느냐로 직관적으로 갈린다.
  • 방어 위치가 다르다: Reflected/Stored는 서버 출력 인코딩, DOM-based는 클라이언트 안전 API.
  • DOM-based의 본질은 source → sink 흐름. 위험한 sink 교체나 source 검증으로 끊는다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.