/** * Security Utilities - 보안 관련 유틸리티 함수 * * XSS 방지, 입력값 검증, 안전한 DOM 조작을 위한 함수 모음 * * @author TK-FB-Project * @since 2026-02-04 */ (function(global) { 'use strict'; const SecurityUtils = { /** * HTML 특수문자 이스케이프 (XSS 방지) * innerHTML에 사용자 입력을 삽입할 때 반드시 사용 * * @param {string} str - 이스케이프할 문자열 * @returns {string} 이스케이프된 문자열 * * @example * element.innerHTML = `${SecurityUtils.escapeHtml(userInput)}`; */ escapeHtml: function(str) { if (str === null || str === undefined) return ''; if (typeof str !== 'string') str = String(str); const htmlEntities = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=' }; return str.replace(/[&<>"'`=\/]/g, function(char) { return htmlEntities[char]; }); }, /** * URL 파라미터 이스케이프 * URL에 사용자 입력을 포함할 때 사용 * * @param {string} str - 이스케이프할 문자열 * @returns {string} URL 인코딩된 문자열 */ escapeUrl: function(str) { if (str === null || str === undefined) return ''; return encodeURIComponent(String(str)); }, /** * JavaScript 문자열 이스케이프 * 동적 JavaScript 생성 시 사용 (권장하지 않음) * * @param {string} str - 이스케이프할 문자열 * @returns {string} 이스케이프된 문자열 */ escapeJs: function(str) { if (str === null || str === undefined) return ''; return String(str) .replace(/\\/g, '\\\\') .replace(/'/g, "\\'") .replace(/"/g, '\\"') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t'); }, /** * 안전한 텍스트 설정 * innerHTML 대신 textContent 사용 권장 * * @param {Element} element - DOM 요소 * @param {string} text - 설정할 텍스트 */ setTextSafe: function(element, text) { if (element && element.nodeType === 1) { element.textContent = text; } }, /** * 안전한 HTML 삽입 * 사용자 입력이 포함된 HTML을 삽입할 때 사용 * * @param {Element} element - DOM 요소 * @param {string} template - HTML 템플릿 ({{변수}} 형식) * @param {Object} data - 삽입할 데이터 (자동 이스케이프됨) * * @example * SecurityUtils.setHtmlSafe(div, '{{name}}', { name: userInput }); */ setHtmlSafe: function(element, template, data) { if (!element || element.nodeType !== 1) return; const self = this; const safeHtml = template.replace(/\{\{(\w+)\}\}/g, function(match, key) { return data.hasOwnProperty(key) ? self.escapeHtml(data[key]) : ''; }); element.innerHTML = safeHtml; }, /** * 입력값 검증 - 숫자 * * @param {any} value - 검증할 값 * @param {Object} options - 옵션 { min, max, allowFloat } * @returns {number|null} 유효한 숫자 또는 null */ validateNumber: function(value, options) { options = options || {}; const num = options.allowFloat ? parseFloat(value) : parseInt(value, 10); if (isNaN(num)) return null; if (options.min !== undefined && num < options.min) return null; if (options.max !== undefined && num > options.max) return null; return num; }, /** * 입력값 검증 - 이메일 * * @param {string} email - 검증할 이메일 * @returns {boolean} 유효 여부 */ validateEmail: function(email) { if (!email || typeof email !== 'string') return false; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); }, /** * 입력값 검증 - 길이 * * @param {string} str - 검증할 문자열 * @param {Object} options - 옵션 { min, max } * @returns {boolean} 유효 여부 */ validateLength: function(str, options) { options = options || {}; if (!str || typeof str !== 'string') return false; const len = str.length; if (options.min !== undefined && len < options.min) return false; if (options.max !== undefined && len > options.max) return false; return true; }, /** * 안전한 JSON 파싱 * * @param {string} jsonString - 파싱할 JSON 문자열 * @param {any} defaultValue - 파싱 실패 시 기본값 * @returns {any} 파싱된 객체 또는 기본값 */ parseJsonSafe: function(jsonString, defaultValue) { defaultValue = defaultValue === undefined ? null : defaultValue; try { return JSON.parse(jsonString); } catch (e) { console.warn('[SecurityUtils] JSON 파싱 실패:', e.message); return defaultValue; } }, /** * localStorage에서 안전하게 데이터 가져오기 * * @param {string} key - 키 * @param {any} defaultValue - 기본값 * @returns {any} 저장된 값 또는 기본값 */ getStorageSafe: function(key, defaultValue) { try { const item = localStorage.getItem(key); if (item === null) return defaultValue; return this.parseJsonSafe(item, defaultValue); } catch (e) { console.warn('[SecurityUtils] localStorage 접근 실패:', e.message); return defaultValue; } }, /** * URL 파라미터 안전하게 가져오기 * * @param {string} name - 파라미터 이름 * @param {string} defaultValue - 기본값 * @returns {string} 파라미터 값 (이스케이프됨) */ getUrlParamSafe: function(name, defaultValue) { defaultValue = defaultValue === undefined ? '' : defaultValue; try { const urlParams = new URLSearchParams(window.location.search); const value = urlParams.get(name); return value !== null ? value : defaultValue; } catch (e) { return defaultValue; } }, /** * ID 파라미터 안전하게 가져오기 (숫자 검증) * * @param {string} name - 파라미터 이름 * @returns {number|null} 유효한 ID 또는 null */ getIdParamSafe: function(name) { const value = this.getUrlParamSafe(name); return this.validateNumber(value, { min: 1 }); }, /** * Content Security Policy 위반 리포터 * * @param {string} reportUri - 리포트 전송 URL */ enableCspReporting: function(reportUri) { document.addEventListener('securitypolicyviolation', function(e) { console.error('[CSP Violation]', { blockedUri: e.blockedURI, violatedDirective: e.violatedDirective, originalPolicy: e.originalPolicy }); if (reportUri) { fetch(reportUri, { method: 'POST', body: JSON.stringify({ blocked_uri: e.blockedURI, violated_directive: e.violatedDirective, document_uri: e.documentURI, timestamp: new Date().toISOString() }), headers: { 'Content-Type': 'application/json' } }).catch(function() {}); } }); } }; // 전역 노출 global.SecurityUtils = SecurityUtils; // 편의를 위한 단축 함수 global.escapeHtml = SecurityUtils.escapeHtml.bind(SecurityUtils); global.escapeUrl = SecurityUtils.escapeUrl.bind(SecurityUtils); console.log('[Module] common/security.js 로드 완료'); })(typeof window !== 'undefined' ? window : this);