tinyMCE 에디터를 사용해서 저장하면 아래와 같이 <html>
태그 형식으로 데이터가 저장됨
예시:
<p>안녕하세요!</p>
<script>alert('XSS');</script>
이러한 방식으로 저장된 데이터는 XSS(크로스 사이트 스크립팅) 공격에 취약할 수 있습니다. 따라서 서버와 클라이언트 모두에서 XSS 방어를 적용해야 합니다.
XSS 방어를 위해 다양한 라이브러리가 존재합니다. 주요 라이브러리를 비교해보면 다음과 같습니다:
라이브러리 | 특징 | 단점 |
---|---|---|
OWASP AntiSamy | 정교한 HTML 필터링 가능, 정책 파일을 통해 세부 조정 가능 | 설정이 복잡하고 성능 부담이 있을 수 있음 |
DOMPurify | 클라이언트에서 사용하기 적합, 즉각적인 XSS 필터링 가능 | 서버 측 필터링 기능이 없음 |
Naver Lucy XSS Filter | 네이버가 개발한 Java 기반의 XSS 필터링 라이브러리로, HTML 및 JavaScript의 악성 코드 삽입을 방지 | 간편하게 사용할 수 있지만, 정책 설정이 제한적일 수 있음 |
HTML Sanitizer | 구글이 제공하는 빠르고 가벼운 HTML 필터링 라이브러리 | 정책 커스터마이징이 제한적일 수 있음 |
Jsoup | 간단한 HTML 파싱 및 필터링 가능 | XSS 공격을 방어하는 데 한계가 있음 |
본 프로젝트에서는 OWASP AntiSamy와 DOMPurify를 함께 사용했습니다.
이렇게 서버와 클라이언트에서 이중으로 XSS 방어를 적용하여 보안을 강화하였습니다.
// OWASP AntiSamy
implementation 'org.owasp.antisamy:antisamy:1.7.7'
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
등의 제목 태그를 허용하도록 수정.
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; // 필터링 실패 시 원본 유지
}
}
}
@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);
}
}
@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("행사 등록에 성공했습니다.");
}
}
<script>alert("XSS")</script>
같은 코드가 제거됨<img>
속성도 제거됨TinyMCE는 <script>
같은 태그를 자동으로 <script>
형태로 변환하여 실행되지 않도록 방어합니다. 하지만 추가적인 보안이 필요합니다.
npm install 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 환경에서 즉시 렌더링 보안 처리가 가능하여, 사용자 화면 보안까지 안전하게 관리할 수 있었음