refactor: strengthen client boundaries and quality gates
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { registerDecorator, type ValidationArguments, type ValidationOptions } from 'class-validator';
|
||||
|
||||
type BoundedJsonOptions = {
|
||||
maxDepth?: number;
|
||||
maxKeys?: number;
|
||||
maxStringLength?: number;
|
||||
};
|
||||
|
||||
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
||||
|
||||
export function IsBoundedJsonObject(options: BoundedJsonOptions = {}, validationOptions?: ValidationOptions) {
|
||||
return (target: object, propertyName: string) =>
|
||||
registerDecorator({
|
||||
name: 'isBoundedJsonObject',
|
||||
target: target.constructor,
|
||||
propertyName,
|
||||
constraints: [options],
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: unknown, args: ValidationArguments) {
|
||||
if (value === undefined || value === null) return true;
|
||||
const [constraints] = args.constraints as [BoundedJsonOptions];
|
||||
return isBoundedJsonValue(value, {
|
||||
maxDepth: constraints.maxDepth ?? 4,
|
||||
maxKeys: constraints.maxKeys ?? 100,
|
||||
maxStringLength: constraints.maxStringLength ?? 2_000,
|
||||
});
|
||||
},
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return `${args.property} contains too many, too deeply nested, or unsafe values`;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function isBoundedJsonValue(value: unknown, limits: Required<BoundedJsonOptions>) {
|
||||
let keyCount = 0;
|
||||
const visit = (current: unknown, depth: number): boolean => {
|
||||
if (depth > limits.maxDepth) return false;
|
||||
if (current == null || typeof current === 'boolean' || typeof current === 'number') return true;
|
||||
if (typeof current === 'string') return current.length <= limits.maxStringLength;
|
||||
if (Array.isArray(current)) {
|
||||
keyCount += current.length;
|
||||
return keyCount <= limits.maxKeys && current.every((item) => visit(item, depth + 1));
|
||||
}
|
||||
if (typeof current !== 'object') return false;
|
||||
const entries = Object.entries(current as Record<string, unknown>);
|
||||
keyCount += entries.length;
|
||||
return (
|
||||
keyCount <= limits.maxKeys &&
|
||||
entries.every(([key, item]) => key.length <= 128 && !FORBIDDEN_KEYS.has(key) && visit(item, depth + 1))
|
||||
);
|
||||
};
|
||||
return visit(value, 0);
|
||||
}
|
||||
Reference in New Issue
Block a user