Skip to content

Commit f649b24

Browse files
authored
fix(utils): normalize() to a max. of 100 levels deep by default (#7957)
This is to avoid infinite recursion that we cannot detect, e.g. when using dynamic proxies.
1 parent 82cebd3 commit f649b24

File tree

2 files changed

+60
-1
lines changed

2 files changed

+60
-1
lines changed

packages/utils/src/normalize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type ObjOrArray<T> = { [key: string]: T };
3333
* @returns A normalized version of the object, or `"**non-serializable**"` if any errors are thrown during normalization.
3434
*/
3535
// eslint-disable-next-line @typescript-eslint/no-explicit-any
36-
export function normalize(input: unknown, depth: number = +Infinity, maxProperties: number = +Infinity): any {
36+
export function normalize(input: unknown, depth: number = 100, maxProperties: number = +Infinity): any {
3737
try {
3838
// since we're at the outermost level, we don't provide a key
3939
return visit('', input, depth, maxProperties);

packages/utils/test/normalize.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,65 @@ describe('normalize()', () => {
8686
expect(normalize(obj)).toEqual({ name: 'Alice', identity: { self: '[Circular ~]' } });
8787
});
8888

89+
test('circular objects with proxy', () => {
90+
const obj1 = { name: 'Alice', child: null } as any;
91+
const obj2 = { name: 'John', child: null } as any;
92+
93+
function getObj1(target: any, prop: string | number | symbol): any {
94+
return prop === 'child'
95+
? new Proxy(obj2, {
96+
get(t, p) {
97+
return getObj2(t, p);
98+
},
99+
})
100+
: target[prop];
101+
}
102+
103+
function getObj2(target: any, prop: string | number | symbol): any {
104+
return prop === 'child'
105+
? new Proxy(obj1, {
106+
get(t, p) {
107+
return getObj1(t, p);
108+
},
109+
})
110+
: target[prop];
111+
}
112+
113+
const proxy1 = new Proxy(obj1, {
114+
get(target, prop) {
115+
return getObj1(target, prop);
116+
},
117+
});
118+
119+
const actual = normalize(proxy1);
120+
121+
// This generates 100 nested objects, as we cannot identify the circular reference since they are dynamic proxies
122+
// However, this test verifies that we can normalize at all, and do not fail out
123+
expect(actual).toEqual({
124+
name: 'Alice',
125+
child: { name: 'John', child: expect.objectContaining({ name: 'Alice', child: expect.any(Object) }) },
126+
});
127+
128+
let last = actual;
129+
for (let i = 0; i < 99; i++) {
130+
expect(last).toEqual(
131+
expect.objectContaining({
132+
name: expect.any(String),
133+
child: expect.any(Object),
134+
}),
135+
);
136+
last = last.child;
137+
}
138+
139+
// Last one is transformed to [Object]
140+
expect(last).toEqual(
141+
expect.objectContaining({
142+
name: expect.any(String),
143+
child: '[Object]',
144+
}),
145+
);
146+
});
147+
89148
test('deep circular objects', () => {
90149
const obj = { name: 'Alice', child: { name: 'Bob' } } as any;
91150
obj.child.self = obj.child;

0 commit comments

Comments
 (0)