xypriss-security
Version:
Advanced High-Performance Security Framework. Military-grade encryption, post-quantum resilience, and fortified data structures.
582 lines • 23.8 kB
JavaScript
;
/**
* 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