Skip to content
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

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

Open
wants to merge 10 commits into
base: alpha
Choose a base branch
from
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 @@ -3862,4 +3863,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.');
});
});
25 changes: 21 additions & 4 deletions src/encode.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,30 @@
import { Op } from './ParseOp';
import ParseRelation from './ParseRelation';

const MAX_RECURSIVE_CALLS = 999;

function encode(
value: mixed,
disallowObjects: boolean,
forcePointers: boolean,
seen: Array<mixed>,
offline: boolean
offline: boolean,
counter: number = 0

Check warning on line 21 in src/encode.js

View check run for this annotation

Codecov / codecov/patch

src/encode.js#L21

Added line #L21 was not covered by tests
): any {
counter++;

if (counter > MAX_RECURSIVE_CALLS) {
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(message);
}

if (value instanceof ParseObject) {
if (disallowObjects) {
throw new Error('Parse Objects not allowed here');
Expand Down Expand Up @@ -67,14 +84,14 @@

if (Array.isArray(value)) {
return value.map(v => {
return encode(v, disallowObjects, forcePointers, seen, offline);
return encode(v, disallowObjects, forcePointers, seen, offline, counter);
});
}

if (value && typeof value === 'object') {
const output = {};
for (const k in value) {
output[k] = encode(value[k], disallowObjects, forcePointers, seen, offline);
output[k] = encode(value[k], disallowObjects, forcePointers, seen, offline, counter);
}
return output;
}
Expand All @@ -89,5 +106,5 @@
seen?: Array<mixed>,
offline?: boolean
): any {
return encode(value, !!disallowObjects, !!forcePointers, seen || [], offline);
return encode(value, !!disallowObjects, !!forcePointers, seen || [], offline, 0);
}
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);
}
}
}
Loading