File size: 4,678 Bytes
8b7b267 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
/**
* HTML Sanitization Utility
* Prevents XSS attacks by escaping HTML special characters
*/
/**
* Escape HTML special characters to prevent XSS
* @param {string|number} text - Text to escape
* @param {boolean} forAttribute - If true, also escapes quotes for HTML attributes
* @returns {string} Escaped HTML string
*/
export function escapeHtml(text, forAttribute = false) {
if (text === null || text === undefined) {
return '';
}
const str = String(text);
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
let escaped = str.replace(/[&<>"']/g, m => map[m]);
// For attributes, ensure quotes are properly escaped
if (forAttribute) {
escaped = escaped.replace(/"/g, '"').replace(/'/g, ''');
}
return escaped;
}
/**
* Safely set innerHTML with sanitization
* @param {HTMLElement} element - DOM element to update
* @param {string} html - HTML string (will be sanitized)
*/
export function safeSetInnerHTML(element, html) {
if (!element || !(element instanceof HTMLElement)) {
console.warn('[Sanitizer] Invalid element provided to safeSetInnerHTML');
return;
}
// For simple text content, use textContent instead
if (!html.includes('<') && !html.includes('>')) {
element.textContent = html;
return;
}
// For HTML content, create a temporary container and sanitize
const temp = document.createElement('div');
temp.innerHTML = html;
// Sanitize all text nodes
const walker = document.createTreeWalker(
temp,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
if (node.textContent) {
node.textContent = node.textContent; // Already safe, but ensure it's set
}
}
// Clear and append sanitized content
element.innerHTML = '';
while (temp.firstChild) {
element.appendChild(temp.firstChild);
}
}
/**
* Sanitize object values for HTML rendering
* Recursively escapes string values in objects
* @param {any} obj - Object to sanitize
* @param {number} depth - Recursion depth limit
* @returns {any} Sanitized object
*/
export function sanitizeObject(obj, depth = 5) {
if (depth <= 0) {
return '[Max Depth Reached]';
}
if (obj === null || obj === undefined) {
return '';
}
if (typeof obj === 'string') {
return escapeHtml(obj);
}
if (typeof obj === 'number' || typeof obj === 'boolean') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => sanitizeObject(item, depth - 1));
}
if (typeof obj === 'object') {
const sanitized = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
sanitized[key] = sanitizeObject(obj[key], depth - 1);
}
}
return sanitized;
}
return String(obj);
}
/**
* Format number safely for display
* @param {number} value - Number to format
* @param {object} options - Formatting options
* @returns {string} Formatted number
*/
export function safeFormatNumber(value, options = {}) {
if (value === null || value === undefined || isNaN(value)) {
return '—';
}
const num = Number(value);
if (isNaN(num)) {
return '—';
}
try {
return num.toLocaleString('en-US', {
minimumFractionDigits: options.minimumFractionDigits || 2,
maximumFractionDigits: options.maximumFractionDigits || 2,
...options
});
} catch (error) {
console.warn('[Sanitizer] Number formatting error:', error);
return String(num);
}
}
/**
* Safely format currency
* @param {number} value - Currency value
* @param {string} currency - Currency code (default: USD)
* @returns {string} Formatted currency string
*/
export function safeFormatCurrency(value, currency = 'USD') {
if (value === null || value === undefined || isNaN(value)) {
return '—';
}
const num = Number(value);
if (isNaN(num)) {
return '—';
}
try {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(num);
} catch (error) {
console.warn('[Sanitizer] Currency formatting error:', error);
return `$${num.toFixed(2)}`;
}
}
|