// @ts-ignore import DOMPurify from 'dompurify' import type { DirectiveBinding } from 'vue' // DOMPurify 설정 타입 interface DOMPurifyConfig { ALLOWED_TAGS: string[] ALLOWED_ATTR: string[] } // 공통 허용 태그 const commonTags = [ 'br', 'div', 'b', 'strong', 'i', 'em', 'u', 'a', 'p', 'ul', 'li', 's', 'ol', 'button', 'span', 'img', 'pre', 'input', 'dl', 'dt', 'dd', 'table', 'thead', 'tbody', 'tfoot', 'video', 'source', 'tr', 'th', 'td', 'section', ] as const // 공통 허용 속성 const commonAttrs = [ 'style', 'id', 'href', 'src', 'target', 'alt', 'class', 'width', 'height', 'frameborder', 'colspan', 'rowspan', 'scope', 'type', 'autoplay', 'muted', 'loop', 'playsinline', 'controls', 'preload', 'poster', ] as const // 기본 설정 (일반 사용자용) const defaultConfig: DOMPurifyConfig = { ALLOWED_TAGS: [...commonTags], ALLOWED_ATTR: [...commonAttrs], } // 관리자용 설정 (확장된 태그 및 속성) const adminConfig: DOMPurifyConfig = { ALLOWED_TAGS: [ ...commonTags, 'iframe', 'link', 'style', 'script', 'blockquote', ], ALLOWED_ATTR: [ ...commonAttrs, 'onclick', 'rel', 'media', 'integrity', 'crossorigin', 'referrerpolicy', 'allowfullscreen', 'exclude-custom', 'exclude', 'defer', ], } /** * HTML 문자열을 안전하게 정화하는 헬퍼 함수 * @param value - 정화할 HTML 문자열 * @param config - DOMPurify 설정 * @returns 정화된 HTML 문자열 */ function sanitizeHtml(value: unknown, config: DOMPurifyConfig): string { // SSR 환경에서는 빈 문자열 반환 if (!import.meta.client) { return '' } // 값이 없거나 문자열이 아닌 경우 빈 문자열 반환 if (!value || typeof value !== 'string') { return '' } try { return DOMPurify.sanitize(value, config) } catch (error) { // 에러 발생 시 원본 값 반환하지 않고 빈 문자열 반환 (보안상 안전) if (import.meta.dev) { // eslint-disable-next-line no-console console.error('[DOMPurify] Sanitization error:', error) } return '' } } /** * 디렉티브 핸들러 생성 함수 * @param config - DOMPurify 설정 * @returns 디렉티브 핸들러 객체 */ function createDirectiveHandler(config: DOMPurifyConfig) { return { mounted(el: HTMLElement, binding: DirectiveBinding) { el.innerHTML = sanitizeHtml(binding.value, config) }, updated(el: HTMLElement, binding: DirectiveBinding) { el.innerHTML = sanitizeHtml(binding.value, config) }, } } export default defineNuxtPlugin(nuxtApp => { // SSR 환경에서는 디렉티브 등록하지 않음 // if (!import.meta.client) { // return // } // 기본 디렉티브: v-dompurify-html nuxtApp.vueApp.directive( 'dompurify-html', createDirectiveHandler(defaultConfig) ) // 관리자용 디렉티브: v-dompurify-admin nuxtApp.vueApp.directive( 'dompurify-admin', createDirectiveHandler(adminConfig) ) })