166 lines
3.1 KiB
TypeScript
166 lines
3.1 KiB
TypeScript
// @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)
|
|
)
|
|
})
|