XSS 방어: OWASP AntiSamy와 DOMPurify를 활용한 TinyMCE 보안 강화

오형상·2025년 2월 22일
0

Ficket

목록 보기
7/8

1. 문제상황

tinyMCE 에디터를 사용해서 저장하면 아래와 같이 <html> 태그 형식으로 데이터가 저장됨

예시:

<p>안녕하세요!</p>
<script>alert('XSS');</script>

이러한 방식으로 저장된 데이터는 XSS(크로스 사이트 스크립팅) 공격에 취약할 수 있습니다. 따라서 서버와 클라이언트 모두에서 XSS 방어를 적용해야 합니다.


2. XSS 방어 라이브러리 비교 및 선택

XSS 방어를 위해 다양한 라이브러리가 존재합니다. 주요 라이브러리를 비교해보면 다음과 같습니다:

라이브러리특징단점
OWASP AntiSamy정교한 HTML 필터링 가능, 정책 파일을 통해 세부 조정 가능설정이 복잡하고 성능 부담이 있을 수 있음
DOMPurify클라이언트에서 사용하기 적합, 즉각적인 XSS 필터링 가능서버 측 필터링 기능이 없음
Naver Lucy XSS Filter네이버가 개발한 Java 기반의 XSS 필터링 라이브러리로, HTML 및 JavaScript의 악성 코드 삽입을 방지간편하게 사용할 수 있지만, 정책 설정이 제한적일 수 있음
HTML Sanitizer구글이 제공하는 빠르고 가벼운 HTML 필터링 라이브러리정책 커스터마이징이 제한적일 수 있음
Jsoup간단한 HTML 파싱 및 필터링 가능XSS 공격을 방어하는 데 한계가 있음

내가 사용한 라이브러리

본 프로젝트에서는 OWASP AntiSamyDOMPurify를 함께 사용했습니다.

  1. OWASP AntiSamy (서버 측 필터링)
    • 정책 파일을 활용하여 허용할 HTML 요소 및 속성을 세부적으로 제어.
    • TinyMCE 에디터에서 입력된 HTML을 검증하고, XSS 공격 요소를 제거.
  2. DOMPurify (클라이언트 측 필터링)
    • 사용자가 입력한 데이터를 렌더링하기 전에 즉각적인 XSS 방어.
    • TinyMCE에서 생성된 HTML을 DOMPurify를 거쳐 필터링 후 브라우저에 출력.

이렇게 서버와 클라이언트에서 이중으로 XSS 방어를 적용하여 보안을 강화하였습니다.


3. 서버에서 XSS 방어 적용하기

라이브러리 설정 (bulid.gradle)

	// OWASP AntiSamy
	implementation 'org.owasp.antisamy:antisamy:1.7.7'

3.2 XML 및 XSD 설정

OWASP AntiSamy를 활용하여 허용할 HTML 태그 및 속성을 정의하는 XML 정책 파일을 설정합니다.

https://github.com/nahsra/antisamy/tree/main/src/main/resources 여기서 설정 파일을 다운받을 수 있는니다.

내가 적용한 방식:
1. antisamy-tinymce.xml, antisamy.xsd을 프로젝트의 resources 폴더에 추가.
2. antisamy-tinymce.xml를 나의 환경에 맞게 커스텀.
3. img 태그에서 src, alt, width, height 속성을 허용하도록 추가.
4. h1, h2, h3 등의 제목 태그를 허용하도록 수정.

3.2 XSSSanitizer 유틸리티 클래스

package com.example.ficketevent.global.utils;

import org.owasp.validator.html.AntiSamy;
import org.owasp.validator.html.CleanResults;
import org.owasp.validator.html.Policy;
import java.io.InputStream;

public class XSSSanitizer {
    private static final Policy policy = loadPolicy();

    private static Policy loadPolicy() {
        try (InputStream is = XSSSanitizer.class.getClassLoader().getResourceAsStream("antisamy-tinymce.xml")) {
            return (is != null) ? Policy.getInstance(is) : null;
        } catch (Exception e) {
            throw new RuntimeException("❌ AntiSamy 정책 파일 로드 실패", e);
        }
    }

    public static String sanitize(String input) {
        if (input == null || input.isBlank() || policy == null) {
            return input;
        }
        try {
            AntiSamy antiSamy = new AntiSamy(policy);
            CleanResults cleanResults = antiSamy.scan(input);
            return cleanResults.getCleanHTML();
        } catch (Exception e) {
            return input; // 필터링 실패 시 원본 유지
        }
    }
}

3.3 DTO에서 XSS 필터 적용

@Getter
public class EventCreateReq {
    private Long companyId;
    private Long stageId;
    private List<Genre> genre;
    private Age age;
    private String content;
    private String title;
    private String subTitle;
    private Integer runningTime;
    private LocalDateTime ticketingTime;
    private Integer reservationLimit;
    private List<EventDateDto> eventDate;
    private List<SeatDto> seats;

    public void sanitizeContent() {
        this.content = XSSSanitizer.sanitize(this.content);
    }
}

3.4 컨트롤러에서 적용

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/events")
public class EventController {

    private final EventService eventService;

    @PostMapping("/admins/event")
    public ResponseEntity<String> registerEvent(@RequestHeader("X-Admin-Id") String adminId, 
                                                 @RequestPart EventCreateReq req, 
                                                 @RequestPart MultipartFile poster, 
                                                 @RequestPart MultipartFile banner) {
        req.sanitizeContent(); // XSS 필터 적용
        eventService.createEvent(Long.parseLong(adminId), req, poster, banner);
        return ResponseEntity.ok("행사 등록에 성공했습니다.");
    }
}

3.5 적용 결과

  • <script>alert("XSS")</script> 같은 코드가 제거됨
  • <img> 속성도 제거됨
  • 안전한 HTML 데이터만 저장됨

4. 클라이언트(React)에서 XSS 방어 적용하기

4.1 TinyMCE에서 기본적으로 XSS 방어 방법

TinyMCE는 <script> 같은 태그를 자동으로 &lt;script&gt; 형태로 변환하여 실행되지 않도록 방어합니다. 하지만 추가적인 보안이 필요합니다.

4.2 DOMPurify 라이브러리 설치

npm install dompurify

4.3 DOMPurify 적용 코드


import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import DOMPurify from "dompurify";
import { eventDetail } from "../../service/event/eventApi";
import { eventDetailStore } from "../../stores/EventStore";

const EventDetail = () => {
  const { eventId } = useParams();
  const event = eventDetailStore();

  useEffect(() => {
    eventDetailGet();
  }, []);

  const eventDetailGet = async () => {
    await eventDetail(
      Number(eventId),
      (response) => {
        event.setEventId(eventId || "");
        event.setContent(response.data.content);
      },
      (_error) => {}
    );
  };

  return (
    <div>
      <h1 className="text-[24px] font-medium mb-[25px]">{event.title}</h1>
      <div
        className="prose"
        dangerouslySetInnerHTML={{
          __html: DOMPurify.sanitize(event.content, {
            FORBID_TAGS: ["svg", "math"], // 불필요한 태그 제한
          }),
        }}
      />
    </div>
  );
};

export default EventDetail;


마무리

  • DOMPurify + OWASP AntiSamy 조합으로 클라이언트와 서버 모두에서 이중 보안 필터링 구조를 성공적으로 구현

  • AntiSamy의 정책 기반 필터링을 통해 <img>, <h2> 등 TinyMCE 사용에 필요한 태그를 유연하게 허용하며 기능 보존과 보안 간의 균형을 고려한 설계를 경험

  • DOMPurify는 React 환경에서 즉시 렌더링 보안 처리가 가능하여, 사용자 화면 보안까지 안전하게 관리할 수 있었음

0개의 댓글