Cross-Site Scripting — OWASP Top 10 단골. user input 이 페이지에 그대로 박혀서 JavaScript 실행. 단순해 보이지만 context 마다 다른 encoding 이 필요하고, modern web 의 SPA / template / framework 가 다 다른 함정. 이 가이드는 XSS 3 type, escape 의 context-sensitivity, CSP / Trusted Types 의 실제 동작을 정리한다.
XSS 3 type
1. Reflected XSS
URL 의 input 이 응답에 그대로 박힘.
https://example.com/search?q=<script>alert(1)</script>
서버 응답 HTML:
<h1>Results for <script>alert(1)</script></h1>
↑
실행됨
공격 시나리오:
공격자 → 위 URL 을 피해자에게 (이메일·메시지)
피해자 클릭 → 자기 쿠키 노출, 자기 권한으로 행동2. Stored XSS
악성 input 이 DB 에 저장 → 모든 사용자에게 전달.
공격자 → comment.body = "<script>steal()</script>" POST
DB 저장
다른 사용자 → 그 페이지 방문 → 모두에게 실행
→ Reflected 보다 훨씬 위험 (한 번 공격으로 전체 사용자 영향)3. DOM-based XSS
서버 거치지 않고 client JavaScript 가 직접 실행.
// 페이지의 JS
const name = new URLSearchParams(location.search).get("name");
document.getElementById("greeting").innerHTML = "Hello, " + name;
// ↑↑↑↑↑↑↑↑↑
// 여기가 폭탄
URL: /?name=<img src=x onerror=alert(1)>
→ innerHTML 이 <img> 를 DOM 으로 → onerror 실행
서버 log 에 안 보임 (URL 의 fragment / hash 면 server 전송 X).Context-sensitive Escape — 가장 흔한 함정
같은 user input 이 어디에 들어가느냐에 따라 escape 방식 다름.
1. HTML 본문 — & < > " ' escape
<div>{user_input}</div>
input: <script>x</script>
escape: <script>x</script>
→ 화면에 글자 그대로 표시, 실행 X2. HTML attribute — 같은 + 따옴표 escape
<input value="{user_input}">
input: " onmouseover="alert(1)
부족한 escape: <input value="" onmouseover="alert(1)">
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
새 attribute 로 parsed
올바른 escape: 따옴표 + 모든 attribute-breaking 문자 escape.3. JavaScript context — JSON encoding
<script>
const data = "{user_input}";
</script>
input: ";alert(1);//
부족한 escape: const data = "";alert(1);//";
↑↑↑↑↑↑↑↑↑↑↑↑
실행됨
올바른 방법: JSON.stringify 후 박기 (또는 따로 attribute 로 전달).
const data = JSON.parse(document.getElementById("data").textContent);4. URL context
<a href="{user_input}">click</a>
input: javascript:alert(1)
→ click 시 JavaScript 실행 (javascript: protocol)
올바른 방법: protocol whitelist (http:, https:, mailto: 만).
encodeURIComponent 만으로는 javascript: 도 통과.→ 모든 context 의 일관 escape 는 불가능. framework 의 context-aware 렌더링 의지 (React JSX, Vue templates, Angular bindings 모두 default 로 escape).
innerHTML 의 위험
element.innerHTML = userInput; // ❌ XSS 위험
대안:
element.textContent = userInput; // ✓ HTML parsing 안 함
element.setAttribute("class", userInput); // attribute escape 자동
용 비교:
// React
<div>{userInput}</div> ✓ 자동 escape
<div dangerouslySetInnerHTML={{__html: userInput}}> ❌ raw HTML
// Vue
<div>{{ userInput }}</div> ✓ 자동 escape
<div v-html="userInput"> ❌ raw HTML
// 모든 *HTML / *raw* / dangerous* 이름은 위험 신호.DOMPurify — 안전한 HTML 허용
사용자가 HTML 입력 허용 (마크다운 → HTML 후, 리치 에디터 등):
const clean = DOMPurify.sanitize(userInput);
element.innerHTML = clean;
DOMPurify 가 하는 일:
- <script>, <iframe>, <object> 등 위험 태그 제거
- onclick, onerror 등 event handler 제거
- javascript: URL 제거
- whitelist 기반 (안전 알려진 것만 통과)
→ 직접 regex 로 <script> 거르기 = 항상 우회 패턴 존재. 라이브러리 의지.Content Security Policy (CSP)
HTTP header 로 "어디서 온 script / style / img 만 OK" 선언.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://apis.google.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
frame-ancestors 'none';
의미:
- script 는 자기 도메인 + Google API 만
- inline <script> 차단 (= XSS payload 99% 차단)
- iframe 안에 박는 거 차단 (clickjacking 방어)
XSS 의 1차 방어:
- script-src 에서 'unsafe-inline' 빼기 (가장 효과 큼)
- nonce 또는 hash 기반 inline 허용
- strict-dynamic 모드 (modern, framework 호환)Trusted Types — modern browser API
Chrome 83+, Firefox/Safari 미지원.
CSP 헤더에 require-trusted-types-for 'script'
→ innerHTML / eval / setTimeout(string) 등에 plain string 거부
→ TrustedHTML / TrustedScript 객체만 허용
→ 객체 만들 때 policy 필수 (sanitize 명시)
policy = trustedTypes.createPolicy("default", {
createHTML: (input) => DOMPurify.sanitize(input),
});
→ "DOMPurify 거치지 않은 HTML 은 절대 박을 수 없게" runtime 강제.관련 도구
- HTML 엔티티 인코딩 / 디코딩 — HTML entity encode/decode (XSS escape 기본 도구)
흔한 함정
- "우리는 React 쓰니 XSS 안전" — dangerouslySetInnerHTML / refs.innerHTML / 외부 라이브러리는 우회. CSP 함께.
- regex 로 escape 시도 — context 마다 다른데 한 regex 로 못 막음. framework 의 context-aware 렌더링 의지.
- frame 안 XSS — same-origin iframe 의 XSS 가 parent 영향. frame-ancestors / sandbox 설정 필요.
- SVG / Markdown 의 XSS — SVG 안 <script>, Markdown 의 raw HTML 허용 → DOMPurify 같이 통과.
- URL parameter trust — 자기 페이지가 자기 URL 쓰는 거 = trust 아님. server-side validate + client-side encode 둘 다.
마무리
XSS 의 본질 = "user input 이 코드처럼 실행". 방어는 context-aware escape + CSP + Trusted Types 의 다층. 한 layer 만 의존 X.
실용 — modern framework (React/Vue/Angular) 의 default escape + DOMPurify (HTML 허용 영역) + 엄격한 CSP (no unsafe-inline) + 보안 헤더. dangerouslySetInnerHTML 같은 escape hatch 사용 시 review 강제.