테마
04. XSS와 CSP·Trusted Types 심층 방어
XSS는 결국 브라우저가 "이 문자열을 텍스트가 아니라 실행 가능한 코드"로 해석하게 만들 때 발생한다. 따라서 방어의 핵심은 출력 컨텍스트를 정확히 다루고, 위험한 DOM API를 통제하는 것이다.
학습 목표
- Stored, Reflected, DOM 기반 XSS의 차이를 설명할 수 있다.
- 출력 컨텍스트별 인코딩이 왜 중요한지 이해한다.
- 프레임워크 자동 이스케이프와 위험 API를 구분할 수 있다.
- CSP와 Trusted Types가 XSS 방어의 추가 방어선임을 설명할 수 있다.
1. XSS는 어디서 생길까?
XSS 유형은 보통 세 가지로 나눈다.
| 유형 | 특징 |
|---|---|
| Stored XSS | 악성 데이터가 저장되고 다른 사용자가 열람할 때 실행 |
| Reflected XSS | 요청값이 응답에 즉시 반사되어 실행 |
| DOM XSS | 서버 응답보다 브라우저의 JavaScript 로직에서 실행 |
현재 실무에서는 DOM XSS 비중도 매우 크다.
즉, 서버 템플릿만 보는 것으로 충분하지 않고 프론트엔드 코드까지 같이 봐야 한다.
2. 기본 방어는 출력 컨텍스트별 인코딩이다
XSS 방어의 출발점은 입력 차단이 아니라 출력 시점 인코딩이다.
안전한 기본 방향
- 텍스트는 텍스트로 렌더링
- 속성은 속성 컨텍스트로 이스케이프
- URL은 URL로 인코딩
- JavaScript 코드 조립은 가능한 한 피함
위험한 방향
- 신뢰하지 않는 문자열을
innerHTML에 넣기 - 템플릿 문자열로 이벤트 핸들러를 만들기
<script>블록 안에 서버 데이터를 직접 이어 붙이기
3. 프레임워크 기본 기능을 먼저 믿고, 함부로 끄지 않는다
대부분의 현대 프레임워크는 기본적으로 HTML 이스케이프를 해 준다.
| 환경 | 기본 안전 장치 | 주의할 API |
|---|---|---|
| React | JSX 텍스트 자동 이스케이프 | dangerouslySetInnerHTML |
| Vue | 템플릿 보간 자동 이스케이프 | v-html |
| Angular | 템플릿 이스케이프 + 일부 DOM 보호 | 우회용 bypassSecurityTrust... 계열 |
| 서버 템플릿 엔진 | 기본 이스케이프 제공 경우가 많음 | raw 출력 필터 |
즉, 시큐어 코딩의 첫걸음은 "프레임워크가 막아주는 것을 내가 우회하고 있지 않은가"를 보는 것이다.
4. HTML을 허용하는 기능은 별도 정책이 필요하다
게시글 본문, CMS, 리치 텍스트 에디터는 HTML을 일부 허용해야 할 수 있다.
이때는 일반 출력 인코딩만으로 해결되지 않는다.
권장 원칙은 다음과 같다.
- HTML이 꼭 필요한 기능인지 먼저 확인
- 허용 태그와 속성을 최소화
- 검증된 Sanitizer 사용
- 가능하면 이미지, 링크, 표 정도로 허용 범위를 좁힘
현재 기준에서 참고할 도구
- DOMPurify
- HTML Purifier
- OWASP Java HTML Sanitizer
- 필요 시 Java 서버 환경에서 Lucy XSS Filter를 제한적으로 검토
반대로 오래된 강의에서 가끔 보이는 ESAPI 중심 권고는 현재 실무 기준에서 우선순위가 낮다.
5. CSP는 두 번째 방어선이다
출력 인코딩이 첫 번째 방어선이라면, CSP(Content Security Policy)는 브라우저에서 실행 가능한 스크립트 출처를 제한하는 두 번째 방어선이다.
http
Content-Security-Policy:
default-src 'self';
script-src 'nonce-<random>' 'strict-dynamic';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';좋은 CSP의 방향은 다음과 같다.
- 인라인 스크립트를 줄인다
nonce기반 스크립트 허용을 사용한다- 가능하면
strict-dynamic으로 nonce를 가진 스크립트 신뢰 사슬을 유지한다 object-src 'none'같은 기본 차단 정책을 둔다- 보고 모드에서 시작해 실제 차단 정책으로 발전시킨다
다만 CSP만 믿고 출력 인코딩을 소홀히 하면 안 된다.
6. Trusted Types는 DOM XSS를 줄이는 데 유리하다
Trusted Types는 브라우저에서 위험한 HTML 삽입 API에 아무 문자열이나 넣지 못하게 돕는 정책이다.
이 기능이 특히 유용한 경우는 다음과 같다.
- 대형 프론트엔드 앱
- 여러 팀이 같은 DOM 조작 코드에 관여하는 환경
innerHTML류 API가 완전히 사라지기 어려운 코드베이스
핵심은 "위험한 DOM sink에 들어가는 값은 검증된 정책을 통과한 값만 허용"하도록 제한하는 것이다.
다만 모든 브라우저에서 동일한 수준으로 적용되는 기능은 아니므로, Trusted Types는 추가 방어선으로 보고 기본 방어는 여전히 출력 인코딩과 CSP에 둬야 한다.
7. HttpOnly만으로는 XSS가 끝나지 않는다
원문에도 세션 탈취 맥락이 나오지만, 현재는 더 넓게 봐야 한다.
HttpOnly는 JavaScript로 쿠키 읽기를 막아 준다- 하지만 XSS가 있으면 사용자는 여전히 악성 요청을 보낼 수 있다
- 계정 변경, 결제, 관리자 동작 같은 민감 조작은 여전히 위험해질 수 있다
즉, HttpOnly는 중요하지만 XSS 자체를 허용해도 된다는 뜻은 아니다.
8. 흔한 잘못된 대응
| 잘못된 대응 | 문제점 | 더 나은 방식 |
|---|---|---|
| 저장 시점에 전체 치환 | 출력 맥락이 바뀌면 다시 위험 | 출력 시점 컨텍스트별 인코딩 |
| 프레임워크 자동 이스케이프 우회 | 안전 기본값을 잃음 | 위험 API 최소화 |
| CSP만 있으면 충분하다고 생각 | DOM 조작, 정책 누락 가능 | 인코딩 + Sanitizer + CSP |
| HTML Sanitizer 없이 사용자 HTML 허용 | 태그와 속성 우회 가능 | 검증된 Sanitizer 사용 |
9. PR 리뷰 체크리스트
- 신뢰하지 않는 문자열이
innerHTML계열 API로 들어가지 않는가 - 서버와 프론트엔드 모두 출력 시점 인코딩을 적용하는가
- HTML 허용 기능에 Sanitizer 정책이 있는가
dangerouslySetInnerHTML,v-html, raw 템플릿 출력이 필요한 이유가 분명한가- CSP가 적용되어 있고, 인라인 스크립트 의존도를 줄이고 있는가
- 민감 동작에 재인증이나 추가 검증이 필요한가
핵심 정리
- XSS는 브라우저가 데이터를 코드로 해석할 때 발생한다
- 기본 방어는 출력 컨텍스트별 인코딩이다
- 프레임워크 자동 이스케이프를 우회하는 API는 매우 신중하게 써야 한다
- CSP와 Trusted Types는 추가 방어선이지만, 기본 인코딩을 대체하지는 못한다