sanitizer.js
1 import sanitizeHtml from "sanitize-html"; 2 import config from "@/config.js"; 3 4 const SanitizerUtils = { 5 sanitizeString: (str, maxLength = null) => { 6 const limit = maxLength ?? config.limits.sanitize.maxStringLength; 7 8 if (!str || typeof str !== "string") { 9 return str; 10 } else if (str.length > limit) { 11 return ""; 12 } 13 14 const normalized = str.normalize("NFC"); 15 const truncated = normalized.slice(0, limit); 16 const sanitized = sanitizeHtml(truncated, config.sanitizer); 17 18 if (sanitized.length === 0 && str.length > 0) { 19 return "[Invalid content]"; 20 } else { 21 return sanitized; 22 } 23 }, 24 25 sanitizeObject: (obj, depth = 0, maxDepth = null) => { 26 const maxDepthLimit = maxDepth ?? config.limits.sanitize.maxDepth; 27 28 if (depth > maxDepthLimit) { 29 return null; 30 } else if (typeof obj === "string") { 31 return SanitizerUtils.sanitizeString(obj, config.limits.sanitize.maxStringLength); 32 } else if (Array.isArray(obj)) { 33 const limitedArray = obj.slice(0, config.limits.sanitize.maxArrayLength); 34 return limitedArray.map(item => SanitizerUtils.sanitizeObject(item, depth + 1, maxDepthLimit)); 35 } else if (obj && typeof obj === "object") { 36 const sanitized = Object.create(null); 37 const keys = Object.keys(obj).slice(0, config.limits.sanitize.maxObjectKeys); 38 for (const key of keys) { 39 if (key === "__proto__" || key === "constructor" || key === "prototype") { 40 continue; 41 } 42 const sanitizedKey = SanitizerUtils.sanitizeString(key, 100); 43 if (sanitizedKey) { 44 sanitized[sanitizedKey] = SanitizerUtils.sanitizeObject(obj[key], depth + 1, maxDepthLimit); 45 } 46 } 47 return Object.freeze(sanitized); 48 } else { 49 return obj; 50 } 51 }, 52 }; 53 54 const JsonUtils = { 55 safeParseJSON: (jsonString, maxSize = null) => { 56 const limit = maxSize ?? config.limits.sanitize.maxJsonSize; 57 const maxDepth = config.limits.sanitize.maxJsonDepth; 58 59 if (typeof jsonString !== "string") { 60 return null; 61 } else if (jsonString.length > limit) { 62 return null; 63 } 64 65 try { 66 const parsed = JSON.parse(jsonString); 67 68 const checkDepth = (obj, currentDepth = 0) => { 69 if (currentDepth > maxDepth) { 70 return false; 71 } else if (Array.isArray(obj)) { 72 return obj.every(item => checkDepth(item, currentDepth + 1)); 73 } else if (obj && typeof obj === "object") { 74 return Object.values(obj).every(value => checkDepth(value, currentDepth + 1)); 75 } else { 76 return true; 77 } 78 }; 79 80 if (!checkDepth(parsed)) { 81 return null; 82 } else { 83 return SanitizerUtils.sanitizeObject(parsed); 84 } 85 } catch { 86 return null; 87 } 88 }, 89 90 getCircularReplacer: () => { 91 const seen = new WeakSet(); 92 return (key, value) => { 93 if (typeof value === "object" && value !== null) { 94 if (seen.has(value)) { 95 return "[Circular]"; 96 } else { 97 seen.add(value); 98 } 99 } 100 return value; 101 }; 102 }, 103 104 safeStringify: (obj) => { 105 try { 106 const sanitized = SanitizerUtils.sanitizeObject(obj); 107 return JSON.stringify(sanitized, JsonUtils.getCircularReplacer()); 108 } catch { 109 return "[Object could not be stringified]"; 110 } 111 }, 112 }; 113 114 export { SanitizerUtils, JsonUtils }; 115 export const sanitizeString = SanitizerUtils.sanitizeString; 116 export const sanitizeObject = SanitizerUtils.sanitizeObject; 117 export const safeParseJSON = JsonUtils.safeParseJSON; 118 export const getCircularReplacer = JsonUtils.getCircularReplacer; 119 export const safeStringify = JsonUtils.safeStringify;