release: WRNexusJS 0.7.0
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { SecurityError } from "./errors.ts";
|
||||
|
||||
const DANGEROUS_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
||||
|
||||
export interface SafeObjectOptions {
|
||||
maxDepth?: number;
|
||||
maxKeys?: number;
|
||||
allowInstances?: boolean;
|
||||
}
|
||||
|
||||
export function isDangerousObjectKey(key: string): boolean {
|
||||
return DANGEROUS_KEYS.has(key);
|
||||
}
|
||||
|
||||
export function assertSafeObject(value: unknown, options: SafeObjectOptions = {}): void {
|
||||
const maxDepth = options.maxDepth ?? 32;
|
||||
const maxKeys = options.maxKeys ?? 10_000;
|
||||
const seen = new WeakSet<object>();
|
||||
let keyCount = 0;
|
||||
|
||||
const visit = (current: unknown, depth: number): void => {
|
||||
if (current === null || typeof current !== "object") return;
|
||||
if (depth > maxDepth) {
|
||||
throw new SecurityError("WRN-SEC-OBJECT-DEPTH", `Object depth exceeds ${maxDepth}.`, 413);
|
||||
}
|
||||
if (seen.has(current)) {
|
||||
throw new SecurityError("WRN-SEC-OBJECT-CYCLE", "Cyclic objects are not accepted.");
|
||||
}
|
||||
seen.add(current);
|
||||
|
||||
const proto = Object.getPrototypeOf(current);
|
||||
if (
|
||||
!options.allowInstances &&
|
||||
!Array.isArray(current) &&
|
||||
proto !== Object.prototype &&
|
||||
proto !== null &&
|
||||
!(current instanceof Date) &&
|
||||
!(current instanceof URL)
|
||||
) {
|
||||
throw new SecurityError(
|
||||
"WRN-SEC-OBJECT-PROTOTYPE",
|
||||
"Only plain objects, arrays, dates, and URLs are accepted.",
|
||||
);
|
||||
}
|
||||
|
||||
for (const key of Object.keys(current)) {
|
||||
keyCount++;
|
||||
if (keyCount > maxKeys) {
|
||||
throw new SecurityError(
|
||||
"WRN-SEC-OBJECT-KEYS",
|
||||
`Object contains more than ${maxKeys} keys.`,
|
||||
413,
|
||||
);
|
||||
}
|
||||
if (isDangerousObjectKey(key)) {
|
||||
throw new SecurityError(
|
||||
"WRN-SEC-PROTOTYPE-POLLUTION",
|
||||
`Dangerous object key '${key}' is not allowed.`,
|
||||
);
|
||||
}
|
||||
visit((current as Record<string, unknown>)[key], depth + 1);
|
||||
}
|
||||
seen.delete(current);
|
||||
};
|
||||
|
||||
visit(value, 0);
|
||||
}
|
||||
|
||||
export function safeMerge<T extends Record<string, unknown>>(
|
||||
target: T,
|
||||
...sources: Array<Record<string, unknown> | undefined | null>
|
||||
): T {
|
||||
for (const source of sources) {
|
||||
if (!source) continue;
|
||||
assertSafeObject(source);
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
if (isDangerousObjectKey(key)) continue;
|
||||
const existing = target[key];
|
||||
if (
|
||||
value &&
|
||||
existing &&
|
||||
typeof value === "object" &&
|
||||
typeof existing === "object" &&
|
||||
!Array.isArray(value) &&
|
||||
!Array.isArray(existing)
|
||||
) {
|
||||
safeMerge(existing as Record<string, unknown>, value as Record<string, unknown>);
|
||||
} else {
|
||||
target[key as keyof T] = value as T[keyof T];
|
||||
}
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
Reference in New Issue
Block a user