/**
* 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);