Skip to content

fix: Fail on infinite recursion in encode.js #2099

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: alpha
Choose a base branch
from
Prev Previous commit
Next Next commit
Added tests
Also included support for the `traverse` function, which experienced the same issue.
  • Loading branch information
ajmeese7 committed Apr 1, 2024
commit e0c322a2c8b59552a40154e05fc07aabb407c2bb
38 changes: 38 additions & 0 deletions src/__tests__/ParseObject-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ const ParseObject = require('../ParseObject').default;
const ParseOp = require('../ParseOp');
const RESTController = require('../RESTController');
const SingleInstanceStateController = require('../SingleInstanceStateController');
const encode = require('../encode').default;
const unsavedChildren = require('../unsavedChildren').default;

const mockXHR = require('./test_helpers/mockXHR');
Expand Down Expand Up @@ -3855,4 +3856,41 @@ describe('ParseObject pin', () => {
});
CoreManager.set('ALLOW_CUSTOM_OBJECT_ID', false);
});

it('handles unsaved circular references', async () => {
const xhrs = [];
RESTController._setXHR(function () {
const xhr = {
setRequestHeader: jest.fn(),
open: jest.fn(),
send: jest.fn(),
status: 200,
readyState: 4,
};
xhrs.push(xhr);
return xhr;
});

const a = {};
const b = {};
a.b = b;
b.a = a;

const object = new ParseObject('Test');
object.set('a', a);
expect(() => {
object.save();
}).toThrowError(
'Maximum recursive calls exceeded in traverse function. Potential infinite recursion detected.'
);
});

it('throws error for infinite recursion', () => {
const circularObject = {};
circularObject.circularReference = circularObject;

expect(() => {
encode(circularObject, false, false, [], false);
}).toThrowError('Maximum recursive calls exceeded in encode function. Potential infinite recursion detected.');
});
});
5 changes: 3 additions & 2 deletions src/encode.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ function encode(
counter++;

if (counter > MAX_RECURSIVE_CALLS) {
console.error('Maximum recursive calls exceeded in encode function. Potential infinite recursion detected.');
const message = 'Maximum recursive calls exceeded in encode function. Potential infinite recursion detected.';
console.error(message);
console.error('Value causing potential infinite recursion:', value);
console.error('Disallow objects:', disallowObjects);
console.error('Force pointers:', forcePointers);
console.error('Seen:', seen);
console.error('Offline:', offline);

throw new Error('Maximum recursive calls exceeded in encode function. Potential infinite recursion detected.');
throw new Error(message);
}

if (value instanceof ParseObject) {
Expand Down
22 changes: 18 additions & 4 deletions src/unsavedChildren.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import ParseFile from './ParseFile';
import ParseObject from './ParseObject';
import ParseRelation from './ParseRelation';

const MAX_RECURSIVE_CALLS = 999;

type EncounterMap = {
objects: { [identifier: string]: ParseObject | boolean },
files: Array<ParseFile>,
Expand Down Expand Up @@ -48,8 +50,20 @@ function traverse(
obj: ParseObject,
encountered: EncounterMap,
shouldThrow: boolean,
allowDeepUnsaved: boolean
allowDeepUnsaved: boolean,
counter: number = 0
) {
counter++;

if (counter > MAX_RECURSIVE_CALLS) {
const message = 'Maximum recursive calls exceeded in traverse function. Potential infinite recursion detected.';
console.error(message);
console.error('Object causing potential infinite recursion:', obj);
console.error('Encountered objects:', encountered);

throw new Error(message);
}

if (obj instanceof ParseObject) {
if (!obj.id && shouldThrow) {
throw new Error('Cannot create a pointer to an unsaved Object.');
Expand All @@ -60,7 +74,7 @@ function traverse(
const attributes = obj.attributes;
for (const attr in attributes) {
if (typeof attributes[attr] === 'object') {
traverse(attributes[attr], encountered, !allowDeepUnsaved, allowDeepUnsaved);
traverse(attributes[attr], encountered, !allowDeepUnsaved, allowDeepUnsaved, counter);
}
}
}
Expand All @@ -78,13 +92,13 @@ function traverse(
if (Array.isArray(obj)) {
obj.forEach(el => {
if (typeof el === 'object') {
traverse(el, encountered, shouldThrow, allowDeepUnsaved);
traverse(el, encountered, shouldThrow, allowDeepUnsaved, counter);
}
});
}
for (const k in obj) {
if (typeof obj[k] === 'object') {
traverse(obj[k], encountered, shouldThrow, allowDeepUnsaved);
traverse(obj[k], encountered, shouldThrow, allowDeepUnsaved, counter);
}
}
}