Skip to content

Commit

Permalink
fix: Fixed infinite ref recursion in some utils functions
Browse files Browse the repository at this point in the history
Fixes rjsf-team#3560 by preventing infinite recursion on `$ref`s
- In `@rjsf/utils` added infinite recursion protection in the `toIdSchema()`, `toPathSchema()` and `getDefaultFormState()` functions
  - Added tests to verify that no infinite recursion due to `$ref`s happen for those three functions along with `retrieveSchema()`
  - Fixed the console.log() call for the `mergeAllOf` exceptions to log the whole error rather than the toString of it
- Updated the `CHANGELOG.md` file accordingly
  • Loading branch information
heath-freenome committed Apr 4, 2023
1 parent 4367379 commit 5788c8b
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 41 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ should change the heading of the (upcoming) version to include a major version b

# 5.5.1

## @rjsf/utils
- Added protections against infinite recursion of `$ref`s for the `toIdSchema()`, `toPathSchema()` and `getDefaultFormState()` functions, fixing [#3560](https://github.com/rjsf-team/react-jsonschema-form/issues/3560)

## Dev / playground

- Refactored some parts of `playground` to make it cleaner
Expand Down
53 changes: 43 additions & 10 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ function maybeAddDefaultToObject<T = any>(
* @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults.
* If "excludeObjectChildren", cause undefined values for this object and pass `includeUndefinedValues` as
* false when computing defaults for any nested object properties.
* @param [_recurseList=[]] - The list of ref names currently being recursed, used to prevent infinite recursion
* @returns - The resulting `formData` with all the defaults provided
*/
export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
Expand All @@ -111,7 +112,8 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
parentDefaults?: T,
rootSchema: S = {} as S,
rawFormData?: T,
includeUndefinedValues: boolean | 'excludeObjectChildren' = false
includeUndefinedValues: boolean | 'excludeObjectChildren' = false,
_recurseList: string[] = []
): T | T[] | undefined {
const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T;
let schema: S = isObject(rawSchema) ? rawSchema : ({} as S);
Expand All @@ -124,9 +126,20 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
} else if (DEFAULT_KEY in schema) {
defaults = schema.default as unknown as T;
} else if (REF_KEY in schema) {
const refName = schema[REF_KEY];
// Use referenced schema defaults for this node.
const refSchema = findSchemaDefinition<S>(schema[REF_KEY]!, rootSchema);
return computeDefaults<T, S, F>(validator, refSchema, defaults, rootSchema, formData as T, includeUndefinedValues);
if (!_recurseList.includes(refName!)) {
const refSchema = findSchemaDefinition<S>(refName, rootSchema);
return computeDefaults<T, S, F>(
validator,
refSchema,
defaults,
rootSchema,
formData as T,
includeUndefinedValues,
_recurseList.concat(refName!)
);
}
} else if (DEPENDENCIES_KEY in schema) {
const resolvedSchema = resolveDependencies<T, S, F>(validator, schema, rootSchema, formData);
return computeDefaults<T, S, F>(
Expand All @@ -135,7 +148,8 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
defaults,
rootSchema,
formData as T,
includeUndefinedValues
includeUndefinedValues,
_recurseList
);
} else if (isFixedItems(schema)) {
defaults = (schema.items! as S[]).map((itemSchema: S, idx: number) =>
Expand All @@ -145,7 +159,8 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
Array.isArray(parentDefaults) ? parentDefaults[idx] : undefined,
rootSchema,
formData as T,
includeUndefinedValues
includeUndefinedValues,
_recurseList
)
) as T[];
} else if (ONE_OF_KEY in schema) {
Expand Down Expand Up @@ -193,7 +208,8 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
get(defaults, [key]),
rootSchema,
get(formData, [key]),
includeUndefinedValues === true
includeUndefinedValues === true,
_recurseList
);
maybeAddDefaultToObject<T>(acc, key, computedDefault, includeUndefinedValues, schema.required);
return acc;
Expand All @@ -209,7 +225,8 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
get(defaults, [key]),
rootSchema,
get(formData, [key]),
includeUndefinedValues === true
includeUndefinedValues === true,
_recurseList
);
maybeAddDefaultToObject<T>(
objectDefaults as GenericObjectType,
Expand All @@ -226,15 +243,23 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
if (Array.isArray(defaults)) {
defaults = defaults.map((item, idx) => {
const schemaItem: S = getInnerSchemaForArrayItem<S>(schema, AdditionalItemsHandling.Fallback, idx);
return computeDefaults<T, S, F>(validator, schemaItem, item, rootSchema);
return computeDefaults<T, S, F>(validator, schemaItem, item, rootSchema, undefined, undefined, _recurseList);
}) as T[];
}

// Deeply inject defaults into already existing form data
if (Array.isArray(rawFormData)) {
const schemaItem: S = getInnerSchemaForArrayItem<S>(schema);
defaults = rawFormData.map((item: T, idx: number) => {
return computeDefaults<T, S, F>(validator, schemaItem, get(defaults, [idx]), rootSchema, item);
return computeDefaults<T, S, F>(
validator,
schemaItem,
get(defaults, [idx]),
rootSchema,
item,
undefined,
_recurseList
);
}) as T[];
}
if (schema.minItems) {
Expand All @@ -246,7 +271,15 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
const fillerSchema: S = getInnerSchemaForArrayItem<S>(schema, AdditionalItemsHandling.Invert);
const fillerDefault = fillerSchema.default;
const fillerEntries: T[] = new Array(schema.minItems - defaultsLength).fill(
computeDefaults<any, S, F>(validator, fillerSchema, fillerDefault, rootSchema)
computeDefaults<any, S, F>(
validator,
fillerSchema,
fillerDefault,
rootSchema,
undefined,
undefined,
_recurseList
)
) as T[];
// then fill up the rest with either the item default or empty, up to minItems
return defaultEntries.concat(fillerEntries);
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/src/schema/retrieveSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export default function retrieveSchema<
deep: false,
} as Options) as S;
} catch (e) {
console.warn('could not merge subschemas in allOf:\n' + e);
console.warn('could not merge subschemas in allOf:\n', e);
const { allOf, ...resolvedSchemaWithoutAllOf } = resolvedSchema;
return resolvedSchemaWithoutAllOf as S;
}
Expand Down
63 changes: 56 additions & 7 deletions packages/utils/src/schema/toIdSchema.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';

import { ALL_OF_KEY, DEPENDENCIES_KEY, ID_KEY, ITEMS_KEY, PROPERTIES_KEY, REF_KEY } from '../constants';
import isObject from '../isObject';
import { FormContextType, IdSchema, RJSFSchema, StrictRJSFSchema, ValidatorType } from '../types';
import retrieveSchema from './retrieveSchema';

/** Generates an `IdSchema` object for the `schema`, recursively
/** An internal helper that generates an `IdSchema` object for the `schema`, recursively with protection against
* infinite recursion
*
* @param validator - An implementation of the `ValidatorType` interface that will be used when necessary
* @param schema - The schema for which the `IdSchema` is desired
Expand All @@ -14,31 +16,54 @@ import retrieveSchema from './retrieveSchema';
* @param [formData] - The current formData, if any, to assist retrieving a schema
* @param [idPrefix='root'] - The prefix to use for the id
* @param [idSeparator='_'] - The separator to use for the path segments in the id
* @param [_recurseList=[]] - The list of retrieved schemas currently being recursed, used to prevent infinite recursion
* @returns - The `IdSchema` object for the `schema`
*/
export default function toIdSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
function toIdSchemaInternal<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
validator: ValidatorType<T, S, F>,
schema: S,
id?: string | null,
rootSchema?: S,
formData?: T,
idPrefix = 'root',
idSeparator = '_'
idSeparator = '_',
_recurseList: S[] = []
): IdSchema<T> {
if (REF_KEY in schema || DEPENDENCIES_KEY in schema || ALL_OF_KEY in schema) {
const _schema = retrieveSchema<T, S, F>(validator, schema, rootSchema, formData);
return toIdSchema<T, S, F>(validator, _schema, id, rootSchema, formData, idPrefix, idSeparator);
const sameSchemaIndex = _recurseList.findIndex((item) => isEqual(item, _schema));
if (sameSchemaIndex === -1) {
return toIdSchemaInternal<T, S, F>(
validator,
_schema,
id,
rootSchema,
formData,
idPrefix,
idSeparator,
_recurseList.concat(_schema)
);
}
}
if (ITEMS_KEY in schema && !get(schema, [ITEMS_KEY, REF_KEY])) {
return toIdSchema<T, S, F>(validator, get(schema, ITEMS_KEY) as S, id, rootSchema, formData, idPrefix, idSeparator);
return toIdSchemaInternal<T, S, F>(
validator,
get(schema, ITEMS_KEY) as S,
id,
rootSchema,
formData,
idPrefix,
idSeparator,
_recurseList
);
}
const $id = id || idPrefix;
const idSchema: IdSchema = { $id } as IdSchema<T>;
if (schema.type === 'object' && PROPERTIES_KEY in schema) {
for (const name in schema.properties) {
const field = get(schema, [PROPERTIES_KEY, name]);
const fieldId = idSchema[ID_KEY] + idSeparator + name;
idSchema[name] = toIdSchema<T, S, F>(
idSchema[name] = toIdSchemaInternal<T, S, F>(
validator,
isObject(field) ? field : {},
fieldId,
Expand All @@ -47,9 +72,33 @@ export default function toIdSchema<T = any, S extends StrictRJSFSchema = RJSFSch
// array item has just been added, but not populated with data yet
get(formData, [name]),
idPrefix,
idSeparator
idSeparator,
_recurseList
);
}
}
return idSchema as IdSchema<T>;
}

/** Generates an `IdSchema` object for the `schema`, recursively
*
* @param validator - An implementation of the `ValidatorType` interface that will be used when necessary
* @param schema - The schema for which the `IdSchema` is desired
* @param [id] - The base id for the schema
* @param [rootSchema] - The root schema, used to primarily to look up `$ref`s
* @param [formData] - The current formData, if any, to assist retrieving a schema
* @param [idPrefix='root'] - The prefix to use for the id
* @param [idSeparator='_'] - The separator to use for the path segments in the id
* @returns - The `IdSchema` object for the `schema`
*/
export default function toIdSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
validator: ValidatorType<T, S, F>,
schema: S,
id?: string | null,
rootSchema?: S,
formData?: T,
idPrefix = 'root',
idSeparator = '_'
): IdSchema<T> {
return toIdSchemaInternal<T, S, F>(validator, schema, id, rootSchema, formData, idPrefix, idSeparator);
}
59 changes: 50 additions & 9 deletions packages/utils/src/schema/toPathSchema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import set from 'lodash/set';

import {
Expand All @@ -17,25 +18,38 @@ import { FormContextType, PathSchema, RJSFSchema, StrictRJSFSchema, ValidatorTyp
import { getClosestMatchingOption } from './index';
import retrieveSchema from './retrieveSchema';

/** Generates an `PathSchema` object for the `schema`, recursively
/** An internal helper that generates an `PathSchema` object for the `schema`, recursively with protection against
* infinite recursion
*
* @param validator - An implementation of the `ValidatorType` interface that will be used when necessary
* @param schema - The schema for which the `PathSchema` is desired
* @param [name=''] - The base name for the schema
* @param [rootSchema] - The root schema, used to primarily to look up `$ref`s
* @param [formData] - The current formData, if any, to assist retrieving a schema
* @param [_recurseList=[]] - The list of retrieved schemas currently being recursed, used to prevent infinite recursion
* @returns - The `PathSchema` object for the `schema`
*/
export default function toPathSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
function toPathSchemaInternal<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
validator: ValidatorType<T, S, F>,
schema: S,
name = '',
rootSchema?: S,
formData?: T
formData?: T,
_recurseList: S[] = []
): PathSchema<T> {
if (REF_KEY in schema || DEPENDENCIES_KEY in schema || ALL_OF_KEY in schema) {
const _schema = retrieveSchema<T, S, F>(validator, schema, rootSchema, formData);
return toPathSchema<T, S, F>(validator, _schema, name, rootSchema, formData);
const sameSchemaIndex = _recurseList.findIndex((item) => isEqual(item, _schema));
if (sameSchemaIndex === -1) {
return toPathSchemaInternal<T, S, F>(
validator,
_schema,
name,
rootSchema,
formData,
_recurseList.concat(_schema)
);
}
}

const pathSchema: PathSchema = {
Expand All @@ -45,13 +59,13 @@ export default function toPathSchema<T = any, S extends StrictRJSFSchema = RJSFS
if (ONE_OF_KEY in schema) {
const index = getClosestMatchingOption<T, S, F>(validator, rootSchema!, formData, schema.oneOf as S[], 0);
const _schema: S = schema.oneOf![index] as S;
return toPathSchema<T, S, F>(validator, _schema, name, rootSchema, formData);
return toPathSchemaInternal<T, S, F>(validator, _schema, name, rootSchema, formData, _recurseList);
}

if (ANY_OF_KEY in schema) {
const index = getClosestMatchingOption<T, S, F>(validator, rootSchema!, formData, schema.anyOf as S[], 0);
const _schema: S = schema.anyOf![index] as S;
return toPathSchema<T, S, F>(validator, _schema, name, rootSchema, formData);
return toPathSchemaInternal<T, S, F>(validator, _schema, name, rootSchema, formData, _recurseList);
}

if (ADDITIONAL_PROPERTIES_KEY in schema && schema[ADDITIONAL_PROPERTIES_KEY] !== false) {
Expand All @@ -60,21 +74,48 @@ export default function toPathSchema<T = any, S extends StrictRJSFSchema = RJSFS

if (ITEMS_KEY in schema && Array.isArray(formData)) {
formData.forEach((element, i: number) => {
pathSchema[i] = toPathSchema<T, S, F>(validator, schema.items as S, `${name}.${i}`, rootSchema, element);
pathSchema[i] = toPathSchemaInternal<T, S, F>(
validator,
schema.items as S,
`${name}.${i}`,
rootSchema,
element,
_recurseList
);
});
} else if (PROPERTIES_KEY in schema) {
for (const property in schema.properties) {
const field = get(schema, [PROPERTIES_KEY, property]);
pathSchema[property] = toPathSchema<T, S, F>(
pathSchema[property] = toPathSchemaInternal<T, S, F>(
validator,
field,
`${name}.${property}`,
rootSchema,
// It's possible that formData is not an object -- this can happen if an
// array item has just been added, but not populated with data yet
get(formData, [property])
get(formData, [property]),
_recurseList
);
}
}
return pathSchema as PathSchema<T>;
}

/** Generates an `PathSchema` object for the `schema`, recursively
*
* @param validator - An implementation of the `ValidatorType` interface that will be used when necessary
* @param schema - The schema for which the `PathSchema` is desired
* @param [name=''] - The base name for the schema
* @param [rootSchema] - The root schema, used to primarily to look up `$ref`s
* @param [formData] - The current formData, if any, to assist retrieving a schema
* @returns - The `PathSchema` object for the `schema`
*/
export default function toPathSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
validator: ValidatorType<T, S, F>,
schema: S,
name = '',
rootSchema?: S,
formData?: T
): PathSchema<T> {
return toPathSchemaInternal(validator, schema, name, rootSchema, formData);
}
11 changes: 11 additions & 0 deletions packages/utils/test/schema/getDefaultFormStateTest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createSchemaUtils, getDefaultFormState, RJSFSchema } from '../../src';
import { computeDefaults } from '../../src/schema/getDefaultFormState';
import { RECURSIVE_REF, RECURSIVE_REF_ALLOF } from '../testUtils/testData';
import { TestValidatorType } from './types';

export default function getDefaultFormStateTest(testValidator: TestValidatorType) {
Expand Down Expand Up @@ -182,6 +183,16 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
{}
);
});
it('test with a recursive schema', () => {
expect(computeDefaults(testValidator, RECURSIVE_REF, undefined, RECURSIVE_REF)).toEqual({
name: '',
});
});
it('test with a recursive allof schema', () => {
expect(computeDefaults(testValidator, RECURSIVE_REF_ALLOF, undefined, RECURSIVE_REF_ALLOF)).toEqual({
value: [undefined],
});
});
});
describe('root default', () => {
it('should map root schema default to form state, if any', () => {
Expand Down
Loading

0 comments on commit 5788c8b

Please sign in to comment.