/** * XSS 방지 및 입력값 살균 유틸리티 * * 보안 목적: * - 사용자 입력에서 위험한 HTML/JS 코드 제거 * - API 응답 데이터의 안전한 렌더링 * - localStorage 데이터의 안전한 파싱 */ /** * HTML 특수문자 이스케이프 * XSS 공격에 사용되는 문자를 안전한 HTML 엔티티로 변환 */ export function escapeHtml(str: string): string { const htmlEscapeMap: Record = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/', } return str.replace(/[&<>"'/]/g, (char) => htmlEscapeMap[char] || char) } /** * HTML 태그 제거 (텍스트만 추출) * dangerouslySetInnerHTML 대신 사용 */ export function stripHtmlTags(html: string): string { return html.replace(/<[^>]*>/g, '') } /** * 안전한 HTML 살균 * 허용된 태그만 남기고 위험한 태그/속성 제거 */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const ALLOWED_TAGS = new Set([ 'b', 'i', 'u', 'strong', 'em', 'br', 'p', 'span', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'sup', 'sub', 'hr', 'blockquote', 'pre', 'code', ]) const DANGEROUS_ATTRS = /\s*on\w+\s*=|javascript\s*:|vbscript\s*:|expression\s*\(/gi export function sanitizeHtml(html: string): string { // 1. script 태그 완전 제거 let sanitized = html.replace(/)<[^<]*)*<\/script>/gi, '') // 2. 위험한 이벤트 핸들러 속성 제거 (onclick, onerror 등) sanitized = sanitized.replace(DANGEROUS_ATTRS, '') // 3. data: URI 스킴 제거 (base64 인코딩된 스크립트 방지) sanitized = sanitized.replace(/\bdata\s*:\s*text\/html/gi, '') // 4. style 속성에서 expression() 제거 (IE CSS XSS) sanitized = sanitized.replace(/style\s*=\s*["'][^"']*expression\s*\([^)]*\)[^"']*["']/gi, '') // 5. iframe, object, embed 태그 제거 sanitized = sanitized.replace(/<\s*(iframe|object|embed|form|input|textarea|button)\b[^>]*>/gi, '') sanitized = sanitized.replace(/<\/\s*(iframe|object|embed|form|input|textarea|button)\s*>/gi, '') return sanitized } /** * 사용자 입력 텍스트 살균 * 게시판, 검색 등에서 사용 */ export function sanitizeInput(input: string, maxLength = 1000): string { if (typeof input !== 'string') return '' return input .trim() .slice(0, maxLength) .replace(/[<>"'`;]/g, '') // 위험 특수문자 제거 } /** * URL 파라미터 안전하게 인코딩 */ export function sanitizeUrlParam(param: string): string { return encodeURIComponent(param) } /** * 안전한 JSON 파싱 (localStorage 등에서 사용) * 파싱 실패 시 기본값 반환 */ export function safeJsonParse(json: string, defaultValue: T): T { try { const parsed = JSON.parse(json) if (parsed === null || parsed === undefined) return defaultValue return parsed as T } catch { return defaultValue } } /** * 안전한 localStorage 읽기 */ export function safeGetLocalStorage(key: string, defaultValue: T): T { try { const raw = localStorage.getItem(key) if (raw === null) return defaultValue return safeJsonParse(raw, defaultValue) } catch { return defaultValue } } /** * 안전한 localStorage 저장 (크기 제한 포함) */ export function safeSetLocalStorage(key: string, value: unknown, maxSizeKB = 5120): boolean { try { const json = JSON.stringify(value) // 크기 제한 검사 (기본 5MB) if (json.length > maxSizeKB * 1024) { console.warn(`localStorage 저장 크기 초과: ${key}`) return false } localStorage.setItem(key, json) return true } catch { return false } } /** * 안전한 PDF 내보내기 (document.write 대체) * Blob URL을 사용하여 새 창에서 콘텐츠 표시 */ export function safePrintHtml(htmlContent: string, title: string, styles: string = ''): void { // HTML 콘텐츠에서 위험 요소 살균 const sanitizedContent = sanitizeHtml(htmlContent) const fullHtml = ` ${escapeHtml(title)} ${styles} ${sanitizedContent}` // Blob URL 사용 (document.write 대체) const blob = new Blob([fullHtml], { type: 'text/html; charset=utf-8' }) const url = URL.createObjectURL(blob) const win = window.open(url, '_blank') // Blob URL 정리 (메모리 누수 방지) if (win) { win.addEventListener('afterprint', () => URL.revokeObjectURL(url)) // 30초 후 자동 정리 setTimeout(() => URL.revokeObjectURL(url), 30000) } else { URL.revokeObjectURL(url) } } /** * 문자열에서 줄바꿈을
로 변환 (안전하게) * dangerouslySetInnerHTML 없이 React 엘리먼트로 반환 */ export function splitByNewline(text: string): string[] { return escapeHtml(text).split(/\n/) }