67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
import { SecurityError } from "./errors.ts";
|
|
import { assertSafeObject, isDangerousObjectKey } from "./object.ts";
|
|
|
|
export interface SecureSerializeOptions {
|
|
maxDepth?: number;
|
|
maxKeys?: number;
|
|
maxBytes?: number;
|
|
redact?: RegExp | string[];
|
|
redactedValue?: string;
|
|
}
|
|
|
|
const DEFAULT_SECRET =
|
|
/(?:password|passwd|secret|token|api[-_]?key|private[-_]?key|otp|authorization|cookie)/i;
|
|
|
|
function shouldRedact(key: string, rule: SecureSerializeOptions["redact"]): boolean {
|
|
if (Array.isArray(rule)) return rule.includes(key);
|
|
return (rule ?? DEFAULT_SECRET).test(key);
|
|
}
|
|
|
|
export function secureJsonStringify(value: unknown, options: SecureSerializeOptions = {}): string {
|
|
assertSafeObject(value, { maxDepth: options.maxDepth, maxKeys: options.maxKeys });
|
|
const redactedValue = options.redactedValue ?? "[REDACTED]";
|
|
|
|
const json = JSON.stringify(value, function secureReplacer(key, current) {
|
|
if (isDangerousObjectKey(key)) {
|
|
throw new SecurityError(
|
|
"WRN-SEC-PROTOTYPE-POLLUTION",
|
|
`Dangerous key '${key}' is not allowed.`,
|
|
);
|
|
}
|
|
if (key && shouldRedact(key, options.redact)) return redactedValue;
|
|
if (typeof current === "bigint") return current.toString();
|
|
if (typeof current === "function" || typeof current === "symbol") {
|
|
throw new SecurityError(
|
|
"WRN-SEC-SERIALIZE-TYPE",
|
|
`Cannot serialize ${typeof current} values.`,
|
|
);
|
|
}
|
|
return current;
|
|
});
|
|
|
|
if (json === undefined) {
|
|
throw new SecurityError("WRN-SEC-SERIALIZE-EMPTY", "Value cannot be serialized.");
|
|
}
|
|
|
|
const safe = json
|
|
.replace(/&/g, "\\u0026")
|
|
.replace(/</g, "\\u003c")
|
|
.replace(/>/g, "\\u003e")
|
|
.replace(/\u2028/g, "\\u2028")
|
|
.replace(/\u2029/g, "\\u2029");
|
|
const bytes = new TextEncoder().encode(safe).byteLength;
|
|
const maxBytes = options.maxBytes ?? 256 * 1024;
|
|
if (bytes > maxBytes) {
|
|
throw new SecurityError(
|
|
"WRN-SEC-SERIALIZE-SIZE",
|
|
`Serialized payload exceeds ${maxBytes} bytes.`,
|
|
413,
|
|
);
|
|
}
|
|
return safe;
|
|
}
|
|
|
|
export function serializeForHtml(value: unknown, options: SecureSerializeOptions = {}): string {
|
|
return secureJsonStringify(value, options);
|
|
}
|