XSS(Cross-Site Scripting)는 공격자가 넣은 스크립트가 내 사이트의 출처(origin) 권한으로 사용자 브라우저에서 실행되는 공격이다. 같은 출처의 코드로 돌기 때문에 동일 출처 정책가 막아 주지 못한다. 그 스크립트는 페이지를 바꾸고, 로그인한 사용자 행세를 하며 요청을 보내고, JS가 읽을 수 있는 데이터(localStorage의 토큰 등)를 빼 갈 수 있다.
어디서 들어오나
| 유형 | 경로 | 예 |
|---|---|---|
| 저장형(Stored) | 서버에 저장된 입력이 다른 사용자에게 그대로 출력됨 | 게시글·댓글·프로필 이름 |
| 반사형(Reflected) | 요청 값이 응답에 바로 되돌아옴 | 검색어를 "○○ 검색 결과"로 출력 |
| DOM 기반(DOM-based) | 서버를 거치지 않고 클라이언트 JS가 입력을 위험한 곳에 넣음 | location.hash를 innerHTML에 대입 |
공통점은 "데이터로 받은 문자열이 코드로 해석되는 곳(sink)에 들어간다"는 것이다. innerHTML, document.write, eval, <a href="javascript:...">, 인라인 이벤트 속성(onerror=)이 대표적이다.
막는 법
- 출력 맥락에 맞게 인코딩: HTML 본문에는
<·>·&·따옴표를 엔티티로, 속성·URL·JS 문자열에는 각 맥락의 규칙으로 바꾼다. 입력 단계에서 걸러내는 것보다 출력 단계에서 바꾸는 것이 기본이다 - 위험한 API를 피한다: 텍스트는
textContent로 넣는다. React는 JSX의{값}을 자동으로 이스케이프하므로 대부분 안전하지만,dangerouslySetInnerHTML은 예외다.href에 사용자 입력으로 들어온javascript:URL은 React 18까지 경고만 하고 그대로 실행됐지만, React 19부터는 실행되지 않는 URL로 바꿔 막는다. 그래도 URL은https:같은 허용 스킴인지 직접 검사하는 편이 안전하다 - HTML을 꼭 받아야 하면 새니타이즈: 서식 있는 편집기 결과처럼 HTML을 허용해야 할 때는 DOMPurify 같은 검증된 라이브러리로 허용 목록 밖의 태그·속성을 지운다. 정규식으로 직접 거르지 않는다
- 피해를 줄이는 장치: 세션 쿠키에
HttpOnly를 주면 스크립트가 쿠키를 읽지 못한다 → HTTP 쿠키. 다만 스크립트가 사용자 대신 요청을 보내는 것까지 막지는 못한다
CSP: 실행 자체를 제한하는 마지막 방어선
CSP(Content Security Policy)는 응답 헤더로 "이 페이지에서 어떤 출처의 스크립트를 실행해도 되는지"를 브라우저에 알리는 정책이다. 인코딩을 놓쳐 스크립트가 주입돼도 정책에 없는 스크립트는 실행되지 않는다.
Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'- 요청마다 새로 만든 예측 불가능한 nonce를
<script nonce="r4nd0m">처럼 붙인 스크립트만 실행된다.'strict-dynamic'은 그렇게 믿은 스크립트가 새로 불러오는 스크립트까지 신뢰를 넘겨 준다. nonce 없는 인라인 스크립트, 인라인 이벤트 속성,javascript:URL,eval은 막힌다 object-src 'none'은 플러그인(<object>·<embed>) 경로를,base-uri 'none'은<base>태그로 상대 경로 스크립트의 출처를 바꾸는 공격을 막는다- 도메인 허용 목록 방식은 바르게 짜기 어렵고 우회에 쓰일 수 있는 도메인이 섞이기 쉬워서, nonce나 해시 기반의 "엄격한 CSP(Strict CSP)"가 권장된다
- 처음에는
Content-Security-Policy-Report-Only헤더(보고 받을 곳은report-to지시어로 지정)로 차단 없이 위반만 보고받으며 깨지는 곳을 찾는다 - CSP는 보완책이다. 인코딩·새니타이즈를 대신하지 않는다
CSRF와의 차이
CSRF는 다른 사이트가 사용자 브라우저를 시켜 요청을 보내게 하는 공격이고, XSS는 내 사이트 안에서 공격자 코드가 도는 공격이다. XSS가 있으면 페이지 안의 CSRF 토큰도 읽을 수 있어 CSRF 방어가 무력해진다. 토큰을 어디에 둘지의 트레이드오프는 JWT, 다른 주입형 공격은 프로토타입 오염을 본다.
명세: W3C — Content Security Policy Level 3: script-src · W3C — CSP Level 3: 'strict-dynamic' · W3C — CSP Level 3: base-uri · W3C — CSP Level 3: Content-Security-Policy-Report-Only 출처: MDN — Cross-site scripting (XSS) · MDN — Content Security Policy: Strict CSP · OWASP — Cross Site Scripting Prevention Cheat Sheet · React — Dangerously setting the inner HTML · React 소스 — sanitizeURL (v19.0.0)