UNPKG

xypriss-security

Version:

Advanced High-Performance Security Framework. Military-grade encryption, post-quantum resilience, and fortified data structures.

582 lines 23.8 kB
"use strict"; /** * Safe Serialization Utility for FortifiedFunction * Handles cyclic structures, XyPriss objects, and performance optimization * * v2 — Improvements over v1: * - Iterative serialization engine (no call-stack growth → supports depth ~10 000+) * - Accurate depth tracking via an explicit node stack (was broken with shared `depth` counter) * - Circular-reference path reporting e.g. "[Circular → $.a.b.c]" * - Chunk-array string builder (avoids O(n²) string concatenation on large outputs) * - Static Set for O(1) lookup of known-problematic constructor names * - Safe UTF-8 boundary truncation (never splits a surrogate pair) * - `parse()` helper with typed return & error guard * - `measureSize()` dry-run to estimate serialized byte size without full output * - `deepClone()` powered by the same safe engine */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SafeSerializer = void 0; // --------------------------------------------------------------------------- // Well-known constructor names that must be replaced before traversal // --------------------------------------------------------------------------- const BLOCKED_CONSTRUCTORS = new Set([ "Socket", "Server", "Agent", "TLSSocket", "Net", "EventEmitter", "ReadStream", "WriteStream", "Transform", "Duplex", ]); const SENSITIVE_HEADERS = new Set([ "authorization", "cookie", "x-api-key", "x-auth-token", "x-session-token", "proxy-authorization", "set-cookie", ]); // --------------------------------------------------------------------------- // SafeSerializer // --------------------------------------------------------------------------- class SafeSerializer { static DEFAULT_OPTIONS = { maxDepth: 10_000, maxLength: 10_000, includeNonEnumerable: false, truncateStrings: 1_000, fastMode: false, maxArrayItems: 10_000, maxObjectKeys: 10_000, reportCircularPath: true, pureRaw: false, }; // ------------------------------------------------------------------------- // PUBLIC API // ------------------------------------------------------------------------- /** * Primary serialization entry-point. * * Fast path: plain JSON.stringify when `fastMode` is enabled and the object * has no known pitfalls. * Safe path: iterative engine that handles depth ~10 000, cycles, specials. */ static stringify(obj, options = {}) { const opts = this.mergeOptions(options); if (opts.fastMode) { try { const result = JSON.stringify(obj); if (result !== undefined && result.length <= opts.maxLength) { return result; } } catch { // Fall through } } return this.iterativeStringify(obj, opts); } /** * XyPriss-aware serialization (req / res objects). * Uses the iterative engine so it is also safe for deeply nested structures. */ static XyPriStringify(obj, options = {}) { // The iterative engine already handles XyPriss objects natively. return this.iterativeStringify(obj, this.mergeOptions(options)); } /** * Safe JSON.parse — never throws; returns `undefined` on failure. */ static parse(json) { try { return JSON.parse(json); } catch { return undefined; } } /** * Estimate serialized size (characters) without building the full string. * Useful to decide whether to serialize at all before hitting maxLength. * Returns -1 when the object is too complex to estimate quickly. */ static measureSize(obj) { try { const s = JSON.stringify(obj); return s === undefined ? -1 : s.length; } catch { return -1; } } /** * Deep-clone a plain-data object through serialization. * Returns `undefined` when the value cannot be round-tripped. */ static deepClone(obj, options = {}) { const serialized = this.stringify(obj, { ...options, maxDepth: 10_000 }); return this.parse(serialized); } /** * Generate a stable cache key for a list of arguments. */ static generateCacheKey(args, prefix = "cache") { if (!args || args.length === 0) return `${prefix}:empty`; const hasXyPriss = args.some((a) => this.isXyPrissObject(a)); if (hasXyPriss) { const safe = this.XyPriStringify(args, { fastMode: false, maxDepth: 3, maxLength: 300, truncateStrings: 50, }); return `${prefix}:xypriss:${safe}`; } try { const simple = JSON.stringify(args); if (simple !== undefined && simple.length <= 500) { return `${prefix}:${simple}`; } } catch { // Fall through } const safe = this.stringify(args, { fastMode: false, maxDepth: 5, maxLength: 500, truncateStrings: 100, }); return `${prefix}:${safe}`; } /** Compact debug log — honours console.log caller location */ static debugLog(label, obj, maxLength = 200) { const serialized = this.stringify(obj, { fastMode: true, maxLength, maxDepth: 3, truncateStrings: 50, }); console.log(`[DEBUG] ${label}: ${serialized}`); } /** Full-fidelity audit log */ static auditLog(obj) { return this.stringify(obj, { fastMode: false, maxDepth: 50, maxLength: 50_000, truncateStrings: 5_000, includeNonEnumerable: false, }); } // ------------------------------------------------------------------------- // CORE: Iterative serialization engine // ------------------------------------------------------------------------- /** * Converts an arbitrary value to a JSON string without using recursion. * * Algorithm: * - Maintain an explicit `stack` of work items. * - Each item knows its expected "output slot" (index into `chunks[]`). * - After processing all children of a container, a "close" marker writes * the closing bracket/brace into the correct slot. * - `seen` is a WeakMap<object, path-string> for O(1) cycle detection with * optional path reporting. * * This avoids JavaScript call-stack growth entirely: depth 10 000 is handled * as cheaply as depth 10. */ static iterativeStringify(root, opts) { const seen = new WeakMap(); const chunks = []; // Write the root value into `chunks` iteratively. this.writeValue(root, "$", 0, seen, chunks, opts); // Join & truncate const result = chunks.join(""); return this.safeTruncate(result, opts.maxLength); } /** * Recursion-free value serializer. * Uses an explicit stack so depth can go to ~10 000 without any JS stack growth. */ static writeValue(value, path, depth, seen, chunks, opts) { const stack = []; const process = (val, p, d) => { let currentVal = val; let currentPath = p; let currentDepth = d; while (true) { // --- Primitives --- if (currentVal === undefined) { chunks.push("null"); return; } if (currentVal === null) { chunks.push("null"); return; } const t = typeof currentVal; if (t === "boolean" || t === "number") { // Guard against non-finite numbers (JSON doesn't support them) if (t === "number" && !isFinite(currentVal)) { chunks.push("null"); } else { chunks.push(JSON.stringify(currentVal)); } return; } if (t === "bigint") { chunks.push(JSON.stringify(currentVal.toString())); return; } if (t === "symbol") { chunks.push(JSON.stringify(`[Symbol:${currentVal.toString()}]`)); return; } if (t === "function") { const fn = currentVal; if (opts.pureRaw) { // In pureRaw, we try to see everything. Functions are objects too! // We mark it as function but allow traversal of its properties const fnObj = { _type: `[Function:${fn.name || "anonymous"}]`, source: fn.toString(), }; // Copy own properties for (const k of Object.getOwnPropertyNames(fn)) { try { fnObj[k] = fn[k]; } catch { } } currentVal = fnObj; continue; } const source = fn.toString(); const snippet = source.length > 100 ? source.substring(0, 100).replace(/\n/g, " ") + "..." : source; chunks.push(JSON.stringify(`[Function:${fn.name || "anonymous"} | ${snippet}]`)); return; } if (t === "string") { const s = currentVal; const truncated = s.length > opts.truncateStrings ? SafeSerializer.safeTruncate(s, opts.truncateStrings) + "...[truncated]" : s; chunks.push(JSON.stringify(truncated)); return; } // --- Objects --- const obj = currentVal; // Depth guard if (currentDepth > opts.maxDepth) { chunks.push(`"[Max Depth: ${currentDepth}]"`); return; } // Cycle detection if (seen.has(obj)) { const circularPath = seen.get(obj); if (opts.reportCircularPath) { chunks.push(`"[Circular → ${circularPath}]"`); } else { chunks.push('"[Circular Reference]"'); } return; } // --- Special value types (no need to mark as seen) --- if (currentVal instanceof Date) { chunks.push(JSON.stringify(currentVal.toISOString())); return; } if (currentVal instanceof RegExp) { chunks.push(JSON.stringify(currentVal.toString())); return; } if (currentVal instanceof Error) { seen.set(obj, currentPath); currentVal = { _type: "[Error]", name: currentVal.name, message: currentVal.message, stack: currentVal.stack ? "[Stack Trace Redacted]" : undefined, }; continue; } if (typeof Buffer !== "undefined" && Buffer.isBuffer(currentVal)) { if (opts.pureRaw) { // Convert buffer to real array for pureRaw inspection currentVal = Array.from(currentVal); continue; } const buf = currentVal; const preview = buf.length > 32 ? buf.slice(0, 32).toString("hex") + "..." : buf.toString("hex"); chunks.push(JSON.stringify(`[Buffer:${buf.length} bytes | 0x${preview}]`)); return; } if (currentVal instanceof Uint8Array || currentVal instanceof ArrayBuffer) { const len = currentVal instanceof ArrayBuffer ? currentVal.byteLength : currentVal.byteLength; chunks.push(`"[BinaryData:${len}bytes]"`); return; } if (currentVal instanceof Map) { seen.set(obj, currentPath); const mapObj = { _type: "[Map]" }; let i = 0; for (const [k, v] of currentVal) { if (i >= opts.maxObjectKeys) { mapObj[`...[${currentVal.size - i} more]`] = null; break; } mapObj[String(k)] = v; i++; } currentVal = mapObj; continue; } if (currentVal instanceof Set) { seen.set(obj, currentPath); currentVal = Array.from(currentVal); continue; } if (currentVal instanceof Promise) { chunks.push('"[Promise]"'); return; } if (currentVal instanceof WeakMap || currentVal instanceof WeakSet || currentVal instanceof WeakRef) { chunks.push(`"[${currentVal.constructor.name}]"`); return; } // --- XyPriss / Node.js special objects --- const ctorName = obj.constructor?.name; if (BLOCKED_CONSTRUCTORS.has(ctorName ?? "")) { if (opts.pureRaw) { // Bypass block and treat as plain object // but we still need to set seen to avoid immediate cycles seen.set(obj, currentPath); // Fall through to plain object traversal below } else { // Extract useful metadata from common blocked objects const meta = { _type: `[Blocked:${ctorName}]` }; try { if (ctorName?.includes("Socket")) { meta.remoteAddress = currentVal.remoteAddress; meta.remotePort = currentVal.remotePort; meta.localPort = currentVal.localPort; } else if (ctorName === "Server") { meta.listening = currentVal.listening; } } catch { } currentVal = meta; continue; } } if ((ctorName === "IncomingMessage" || ctorName === "Request") && !currentVal._type) { seen.set(obj, currentPath); currentVal = { _type: "[XyPriss Request]", method: currentVal.method, url: currentVal.url, headers: SafeSerializer.sanitizeHeaders(currentVal.headers), query: currentVal.query, params: currentVal.params, body: currentVal.body ? "[Request Body]" : undefined, ip: currentVal.ip, }; continue; } if ((ctorName === "ServerResponse" || ctorName === "Response") && !currentVal._type) { seen.set(obj, currentPath); currentVal = { _type: "[XyPriss Response]", statusCode: currentVal.statusCode, statusMessage: currentVal.statusMessage, headersSent: currentVal.headersSent, }; continue; } // Heuristic: looks like a duck-typed XyPriss request (Axios config, etc) if (currentVal.method && currentVal.url && currentVal.headers && !Array.isArray(currentVal) && !currentVal._type) { seen.set(obj, currentPath); currentVal = { _type: "[XyPriss Request-like]", method: currentVal.method, url: currentVal.url, headers: SafeSerializer.sanitizeHeaders(currentVal.headers), }; continue; } // --- Arrays --- if (Array.isArray(currentVal)) { seen.set(obj, currentPath); const arr = currentVal; if (arr.length === 0) { chunks.push("[]"); return; } const limit = Math.min(arr.length, opts.maxArrayItems); const truncatedArray = limit < arr.length; chunks.push("["); // Push items onto the stack in reverse order so they execute in order // We schedule a "close" task last (it runs after all items) const closeIdx = chunks.length; // slot reserved below chunks.push(""); // placeholder for closing bracket / truncation note const itemTasks = []; for (let i = 0; i < limit; i++) { const idx = i; itemTasks.push(() => { if (idx > 0) chunks.push(","); process(arr[idx], `${currentPath}[${idx}]`, currentDepth + 1); }); } // The close task const closeTask = () => { if (truncatedArray) { chunks.push(","); chunks.push(`"...[${arr.length - limit} more items truncated]"`); } chunks[closeIdx] = ""; // clear placeholder chunks.push("]"); }; // Push close task first, then items in REVERSE order so Task(0) is on top stack.push(closeTask); for (let i = itemTasks.length - 1; i >= 0; i--) { stack.push(itemTasks[i]); } return; } // --- Plain objects --- seen.set(obj, currentPath); const keys = opts.includeNonEnumerable ? Object.getOwnPropertyNames(obj) : Object.keys(obj); // Filter out deprecated Node internal properties to avoid warnings like DEP0066 const filteredKeys = keys.filter((k) => k !== "_headerNames"); if (filteredKeys.length === 0) { chunks.push("{}"); return; } const limit = Math.min(filteredKeys.length, opts.maxObjectKeys); const truncatedObj = limit < filteredKeys.length; chunks.push("{"); const closeIdx = chunks.length; chunks.push(""); // placeholder const keyTasks = []; for (let i = 0; i < limit; i++) { const k = filteredKeys[i]; const idx = i; keyTasks.push(() => { if (idx > 0) chunks.push(","); chunks.push(JSON.stringify(k)); chunks.push(":"); let v; try { v = obj[k]; } catch { v = "[Property Access Error]"; } process(v, `${currentPath}.${k}`, currentDepth + 1); }); } const closeTask = () => { if (truncatedObj) { chunks.push(","); chunks.push(`"...[${keys.length - limit} more keys truncated]":null`); } chunks[closeIdx] = ""; chunks.push("}"); }; stack.push(closeTask); for (let i = keyTasks.length - 1; i >= 0; i--) { stack.push(keyTasks[i]); } return; } }; // Seed the stack with the root stack.push(() => process(value, path, depth)); // Drain the stack — this is the non-recursive loop while (stack.length > 0) { const task = stack.pop(); task(); } } // ------------------------------------------------------------------------- // UTILITIES // ------------------------------------------------------------------------- /** Merge user options with defaults, always producing a fully-defined object */ static mergeOptions(options) { return { ...this.DEFAULT_OPTIONS, ...options }; } /** * Truncate a string at a safe Unicode boundary (no split surrogates). */ static safeTruncate(s, maxLen) { if (s.length <= maxLen) return s; // Walk back from maxLen until we find a non-low-surrogate boundary let i = maxLen; while (i > 0 && s.charCodeAt(i) >= 0xdc00 && s.charCodeAt(i) <= 0xdfff) { i--; } return s.substring(0, i); } /** Redact sensitive HTTP headers */ static sanitizeHeaders(headers) { if (!headers || typeof headers !== "object") return headers; const sanitized = {}; for (const [k, v] of Object.entries(headers)) { sanitized[k] = SENSITIVE_HEADERS.has(k.toLowerCase()) ? "[REDACTED]" : v; } return sanitized; } /** Duck-type detection for XyPriss req/res objects */ static isXyPrissObject(arg) { if (!arg || typeof arg !== "object") return false; const a = arg; const name = a.constructor?.name; if (name === "IncomingMessage" || name === "ServerResponse" || name === "Request" || name === "Response") return true; if (a.method && a.url && a.headers) return true; if (a.statusCode !== undefined && a.headersSent !== undefined) return true; return false; } } exports.SafeSerializer = SafeSerializer; //# sourceMappingURL=safe-serializer.js.map