From ce72f3638c9fe70ef0e31c64cba112f2919d39eb Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Mon, 19 Aug 2024 14:44:03 -0400 Subject: [PATCH 01/17] remove usage of omit that depends on lodash --- .../Linodes/LinodeCreatev2/utilities.ts | 4 +-- .../src/utilities/formikErrorUtils.test.ts | 26 +++++++++---------- packages/manager/src/utilities/set.ts | 15 +++++++++++ 3 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 packages/manager/src/utilities/set.ts diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts index 2dcce778c29..8bbde781543 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts @@ -1,4 +1,3 @@ -import { omit } from 'lodash'; import { useHistory } from 'react-router-dom'; import { imageQueries } from 'src/queries/images'; @@ -7,6 +6,7 @@ import { stackscriptQueries } from 'src/queries/stackscripts'; import { sendCreateLinodeEvent } from 'src/utilities/analytics/customEventAnalytics'; import { privateIPRegex } from 'src/utilities/ipUtils'; import { isNotNullOrUndefined } from 'src/utilities/nullOrUndefined'; +import { omitProps } from 'src/utilities/omittedProps'; import { getQueryParamsFromQueryString } from 'src/utilities/queryParams'; import { utoa } from '../LinodesCreate/utilities'; @@ -144,7 +144,7 @@ export const tabs: LinodeCreateType[] = [ export const getLinodeCreatePayload = ( formValues: LinodeCreateFormValues ): CreateLinodeRequest => { - const values = omit(formValues, ['linode', 'hasSignedEUAgreement']); + const values = omitProps(formValues, ['linode', 'hasSignedEUAgreement']); if (values.metadata?.user_data) { values.metadata.user_data = utoa(values.metadata.user_data); } diff --git a/packages/manager/src/utilities/formikErrorUtils.test.ts b/packages/manager/src/utilities/formikErrorUtils.test.ts index 3b7df4ce74b..245fd29361b 100644 --- a/packages/manager/src/utilities/formikErrorUtils.test.ts +++ b/packages/manager/src/utilities/formikErrorUtils.test.ts @@ -35,51 +35,51 @@ describe('handleAPIErrors', () => { const subnetMultipleErrorsPerField = [ { - reason: 'not expected error for label', field: 'subnets[0].label', + reason: 'not expected error for label', }, { - reason: 'expected error for label', field: 'subnets[0].label', + reason: 'expected error for label', }, { - reason: 'not expected error for label', field: 'subnets[3].label', + reason: 'not expected error for label', }, { - reason: 'expected error for label', field: 'subnets[3].label', + reason: 'expected error for label', }, { - reason: 'not expected error for ipv4', field: 'subnets[3].ipv4', + reason: 'not expected error for ipv4', }, { - reason: 'expected error for ipv4', field: 'subnets[3].ipv4', + reason: 'expected error for ipv4', }, ]; const subnetErrors = [ { - reason: 'Label required', field: 'subnets[1].label', + reason: 'Label required', }, { - reason: 'bad label', field: 'subnets[2].label', + reason: 'bad label', }, { - reason: 'cidr ipv4', field: 'subnets[2].ipv4', + reason: 'cidr ipv4', }, { - reason: 'needs an ip', field: 'subnets[4].ipv4', + reason: 'needs an ip', }, { - reason: 'needs an ipv6', field: 'subnets[4].ipv6', + reason: 'needs an ipv6', }, ]; @@ -93,7 +93,7 @@ describe('handleVpcAndConvertSubnetErrors', () => { expect(Object.keys(errors)).toHaveLength(3); expect(Object.keys(errors)).toEqual(['1', '2', '4']); expect(errors[1]).toEqual({ label: 'Label required' }); - expect(errors[2]).toEqual({ label: 'bad label', ipv4: 'cidr ipv4' }); + expect(errors[2]).toEqual({ ipv4: 'cidr ipv4', label: 'bad label' }); expect(errors[4]).toEqual({ ipv4: 'needs an ip', ipv6: 'needs an ipv6' }); }); @@ -106,8 +106,8 @@ describe('handleVpcAndConvertSubnetErrors', () => { expect(Object.keys(errors)).toHaveLength(2); expect(errors[0]).toEqual({ label: 'expected error for label' }); expect(errors[3]).toEqual({ - label: 'expected error for label', ipv4: 'expected error for ipv4', + label: 'expected error for label', }); }); diff --git a/packages/manager/src/utilities/set.ts b/packages/manager/src/utilities/set.ts new file mode 100644 index 00000000000..531d09c275e --- /dev/null +++ b/packages/manager/src/utilities/set.ts @@ -0,0 +1,15 @@ +// oh boy oh boy fear + +/** + * Helper to set the given value at the given path of the given object. + * This util is a direct replacement for `set` from lodash. [maybe we will see i'm kinda afraid ngl] + * + * @param object — The object to modify. + * @param path — The path of the property to set. + * @param value — The value to set. + * @return — Returns object. + */ +export const set = () => { + // must check for proto / constructor / prototype + return; +}; From 630c709bb86e7ab1abca57af2dfecbaaf0936e6e Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 20 Aug 2024 14:30:34 -0400 Subject: [PATCH 02/17] it is not going --- packages/manager/src/utilities/set.ts | 79 +++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/utilities/set.ts b/packages/manager/src/utilities/set.ts index 531d09c275e..2c925f925a3 100644 --- a/packages/manager/src/utilities/set.ts +++ b/packages/manager/src/utilities/set.ts @@ -1,15 +1,84 @@ -// oh boy oh boy fear +type PropertyName = number | string | symbol; +type PropertyPath = PropertyName | readonly PropertyName[]; /** * Helper to set the given value at the given path of the given object. - * This util is a direct replacement for `set` from lodash. [maybe we will see i'm kinda afraid ngl] + * This util is a direct replacement for `set` from lodash, with additional + * checks to protect against prototype pollution. * * @param object — The object to modify. * @param path — The path of the property to set. * @param value — The value to set. * @return — Returns object. */ -export const set = () => { - // must check for proto / constructor / prototype - return; +export function set( + object: T, + path: PropertyPath, + value: any +): T { + // ensure that object is actually an object + if (!object || Array.isArray(object) || typeof object !== 'object') { + return object; + } + + // if the path is not an array, convert it to an array format + const updatedPath: PropertyName[] = Array.isArray(path) + ? path + : path.toString().match(/[^.[\]]+/g) ?? []; + + if ( + // ensure that both the path and value will not lead to prototype pollution issues + !updatedPath.reduce( + (acc, curKey) => acc && isStringPrototypePollutionSafe(curKey.toString()), + true + ) || + !isValuePrototypePollutionSafe(value) + ) { + return object; + } + + for (let i = 0; i < updatedPath.length - 1; i++) { + // oh boy + } + + return object; +} + +/** + * Helper to ensure a value cannot lead to a prototype pollution issue. + * Note that this is made with the specific use case of checking that some + * value to be set inside an object is safe. + * + * @param value - The value to check + * @return - If value is safe, returns it; otherwise returns undefined + */ +export const isValuePrototypePollutionSafe = (value: any): boolean => { + // An array is safe if all of its value are safe + if (Array.isArray(value) && value.length > 0) { + return ( + isValuePrototypePollutionSafe(value[0]) && + isValuePrototypePollutionSafe(value.splice(1)) + ); + } + + // An object is safe if all of its values and keys are safe + if (value && typeof value === 'object') { + return ( + Object.keys(value).reduce( + (acc, curKey) => acc && isStringPrototypePollutionSafe(curKey), + true + ) && isValuePrototypePollutionSafe(Object.values(value)) + ); + } + + // If the value we are checking is not an array/object, we assume it to be safe + return true; +}; + +/** + * Determines if the given string is prototype pollution safe. This assumes the context + * that the string being passed in is a potential key for some object. + */ +const isStringPrototypePollutionSafe = (key: string) => { + return key !== '__proto__' && key !== 'prototype' && key !== 'constructor'; }; From d002981f2ac38f5afe6234b3e5f6bde7fb7941c9 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 20 Aug 2024 16:18:45 -0400 Subject: [PATCH 03/17] need to figure out types --- packages/manager/src/utilities/set.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/utilities/set.ts b/packages/manager/src/utilities/set.ts index 2c925f925a3..d75e59187b7 100644 --- a/packages/manager/src/utilities/set.ts +++ b/packages/manager/src/utilities/set.ts @@ -22,14 +22,14 @@ export function set( } // if the path is not an array, convert it to an array format - const updatedPath: PropertyName[] = Array.isArray(path) - ? path + const updatedPath: string[] = Array.isArray(path) + ? path.map((key) => key.toString()) : path.toString().match(/[^.[\]]+/g) ?? []; if ( // ensure that both the path and value will not lead to prototype pollution issues !updatedPath.reduce( - (acc, curKey) => acc && isStringPrototypePollutionSafe(curKey.toString()), + (acc, curKey) => acc && isStringPrototypePollutionSafe(curKey), true ) || !isValuePrototypePollutionSafe(value) @@ -37,9 +37,19 @@ export function set( return object; } - for (let i = 0; i < updatedPath.length - 1; i++) { - // oh boy - } + // let updatingObject = object; + // for (let i = 0; i < updatedPath.length - 1; i++) { + // const key = updatedPath[i]; + // if (updatingObject[key as keyof {}] && typeof updatingObject[key as keyof {}] === 'object') { + // updatingObject = updatingObject[key as keyof {}]; + // } else { + // const nextKey = updatedPath[i + 1]; + // updatingObject[key as keyof {}] = []; // or {} + // updatingObject = updatingObject[key as keyof {}]; + // } + // } + // // set value after reaching end of the path + // updatingObject[updatedPath[updatedPath.length - 1] as keyof {}] = value; return object; } From 71b15b417e157288916232b6e120514bb83a2acb Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 20 Aug 2024 17:07:39 -0400 Subject: [PATCH 04/17] hopefully this is something --- packages/manager/src/utilities/set.test.ts | 17 ++++++++++ packages/manager/src/utilities/set.ts | 36 +++++++++++----------- 2 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 packages/manager/src/utilities/set.test.ts diff --git a/packages/manager/src/utilities/set.test.ts b/packages/manager/src/utilities/set.test.ts new file mode 100644 index 00000000000..97b85995e5e --- /dev/null +++ b/packages/manager/src/utilities/set.test.ts @@ -0,0 +1,17 @@ +import { isValuePrototypePollutionSafe, set } from './set'; + +describe('Tests for set', () => { + it('sets the value at the given path', () => { + const object: any = {}; + set(object, 'test', 1); + expect(object.test).toBe(1); + }); +}); + +describe('Tests for isValuePrototypePollutionSafe', () => { + it('checks if the given array is prototype pollution safe', () => { + expect(isValuePrototypePollutionSafe([])).toBe(true); + }); +}); + +// there will definitely be (A LOT) more tests diff --git a/packages/manager/src/utilities/set.ts b/packages/manager/src/utilities/set.ts index d75e59187b7..c5a44bad6e0 100644 --- a/packages/manager/src/utilities/set.ts +++ b/packages/manager/src/utilities/set.ts @@ -22,14 +22,14 @@ export function set( } // if the path is not an array, convert it to an array format - const updatedPath: string[] = Array.isArray(path) - ? path.map((key) => key.toString()) + const updatedPath: PropertyName[] = Array.isArray(path) + ? path : path.toString().match(/[^.[\]]+/g) ?? []; if ( // ensure that both the path and value will not lead to prototype pollution issues !updatedPath.reduce( - (acc, curKey) => acc && isStringPrototypePollutionSafe(curKey), + (acc, curKey) => acc && isStringPrototypePollutionSafe(curKey.toString()), true ) || !isValuePrototypePollutionSafe(value) @@ -37,19 +37,19 @@ export function set( return object; } - // let updatingObject = object; - // for (let i = 0; i < updatedPath.length - 1; i++) { - // const key = updatedPath[i]; - // if (updatingObject[key as keyof {}] && typeof updatingObject[key as keyof {}] === 'object') { - // updatingObject = updatingObject[key as keyof {}]; - // } else { - // const nextKey = updatedPath[i + 1]; - // updatingObject[key as keyof {}] = []; // or {} - // updatingObject = updatingObject[key as keyof {}]; - // } - // } - // // set value after reaching end of the path - // updatingObject[updatedPath[updatedPath.length - 1] as keyof {}] = value; + let updatingObject: any = object; + for (let i = 0; i < updatedPath.length - 1; i++) { + const key = updatedPath[i]; + if (!updatingObject[key] || typeof updatingObject[key] !== 'object') { + const nextKey = updatedPath[i + 1]; + updatingObject[key] = Number.isNaN(nextKey) ? {} : []; + } + + updatingObject = updatingObject[key]; + } + + // set value after reaching end of the path + updatingObject[updatedPath[updatedPath.length - 1]] = value; return object; } @@ -71,8 +71,8 @@ export const isValuePrototypePollutionSafe = (value: any): boolean => { ); } - // An object is safe if all of its values and keys are safe - if (value && typeof value === 'object') { + // An object (that is not an array) is safe if all of its values and keys are safe + if (value && !Array.isArray(value) && typeof value === 'object') { return ( Object.keys(value).reduce( (acc, curKey) => acc && isStringPrototypePollutionSafe(curKey), From 82f904d55ee72195396e79252904ce7988b452ea Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 20 Aug 2024 17:59:05 -0400 Subject: [PATCH 05/17] todo: fix all bugs and write all the tests --- packages/manager/src/utilities/set.test.ts | 32 ++++++++++++++++++++-- packages/manager/src/utilities/set.ts | 28 +++++++------------ 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/packages/manager/src/utilities/set.test.ts b/packages/manager/src/utilities/set.test.ts index 97b85995e5e..6c322881d12 100644 --- a/packages/manager/src/utilities/set.test.ts +++ b/packages/manager/src/utilities/set.test.ts @@ -9,8 +9,36 @@ describe('Tests for set', () => { }); describe('Tests for isValuePrototypePollutionSafe', () => { - it('checks if the given array is prototype pollution safe', () => { - expect(isValuePrototypePollutionSafe([])).toBe(true); + it('determines that given array is prototype pollution safe', () => { + // expect(isValuePrototypePollutionSafe([])).toBe(true); + // expect(isValuePrototypePollutionSafe(['', 'test'])).toBe(true); + // expect(isValuePrototypePollutionSafe(['', 1])).toBe(true); + // expect(isValuePrototypePollutionSafe(['', {}, { test: 'test' }, 3])).toBe( + // true + // ); + }); + it('determines that the given array is not prototype pollution safe', () => { + expect(isValuePrototypePollutionSafe(['__proto__'])).toBe(false); + expect(isValuePrototypePollutionSafe(['', 'test', 'prototype'])).toBe( + false + ); + expect(isValuePrototypePollutionSafe(['', 1, 'constructor'])).toBe(false); + expect( + isValuePrototypePollutionSafe(['', 1, 'constructor', '__proto__']) + ).toBe(false); + // expect(isValuePrototypePollutionSafe(['', 1, { __proto__: 'test' }])).toBe( + // false + // ); + expect( + isValuePrototypePollutionSafe(['', 1, { constructor: 'test' }]) + ).toBe(false); + expect(isValuePrototypePollutionSafe(['', 1, { prototype: 'test' }])).toBe( + false + ); + // expect( + // isValuePrototypePollutionSafe([{ test: { test: { __proto__: 'test' } } }]) + // ).toBe(false); + // sadcat moment }); }); diff --git a/packages/manager/src/utilities/set.ts b/packages/manager/src/utilities/set.ts index c5a44bad6e0..c5594bf215e 100644 --- a/packages/manager/src/utilities/set.ts +++ b/packages/manager/src/utilities/set.ts @@ -28,10 +28,7 @@ export function set( if ( // ensure that both the path and value will not lead to prototype pollution issues - !updatedPath.reduce( - (acc, curKey) => acc && isStringPrototypePollutionSafe(curKey.toString()), - true - ) || + !isValuePrototypePollutionSafe(updatedPath) || !isValuePrototypePollutionSafe(value) ) { return object; @@ -42,6 +39,7 @@ export function set( const key = updatedPath[i]; if (!updatingObject[key] || typeof updatingObject[key] !== 'object') { const nextKey = updatedPath[i + 1]; + // this line has to be changed, because "01" should lead to an object's keys, not an array updatingObject[key] = Number.isNaN(nextKey) ? {} : []; } @@ -56,13 +54,17 @@ export function set( /** * Helper to ensure a value cannot lead to a prototype pollution issue. - * Note that this is made with the specific use case of checking that some - * value to be set inside an object is safe. * * @param value - The value to check * @return - If value is safe, returns it; otherwise returns undefined */ export const isValuePrototypePollutionSafe = (value: any): boolean => { + if (typeof value === 'string') { + return ( + value !== '__proto__' && value !== 'prototype' && value !== 'constructor' + ); + } + // An array is safe if all of its value are safe if (Array.isArray(value) && value.length > 0) { return ( @@ -74,21 +76,11 @@ export const isValuePrototypePollutionSafe = (value: any): boolean => { // An object (that is not an array) is safe if all of its values and keys are safe if (value && !Array.isArray(value) && typeof value === 'object') { return ( - Object.keys(value).reduce( - (acc, curKey) => acc && isStringPrototypePollutionSafe(curKey), - true - ) && isValuePrototypePollutionSafe(Object.values(value)) + isValuePrototypePollutionSafe(Object.keys(value)) && + isValuePrototypePollutionSafe(Object.values(value)) ); } // If the value we are checking is not an array/object, we assume it to be safe return true; }; - -/** - * Determines if the given string is prototype pollution safe. This assumes the context - * that the string being passed in is a potential key for some object. - */ -const isStringPrototypePollutionSafe = (key: string) => { - return key !== '__proto__' && key !== 'prototype' && key !== 'constructor'; -}; From c0320a9a6eee36b84fce764efaebfe1fd49cb9c1 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 21 Aug 2024 15:58:10 -0400 Subject: [PATCH 06/17] write tests - need to debug --- packages/manager/src/utilities/set.test.ts | 299 ++++++++++++++++++--- packages/manager/src/utilities/set.ts | 25 +- 2 files changed, 277 insertions(+), 47 deletions(-) diff --git a/packages/manager/src/utilities/set.test.ts b/packages/manager/src/utilities/set.test.ts index 6c322881d12..5e4f70ff046 100644 --- a/packages/manager/src/utilities/set.test.ts +++ b/packages/manager/src/utilities/set.test.ts @@ -1,44 +1,281 @@ -import { isValuePrototypePollutionSafe, set } from './set'; +import _set from 'lodash.set'; + +import { isKeyPrototypePollutionSafe, set } from './set'; + +// todo: as i debug get rid of calls to _set describe('Tests for set', () => { - it('sets the value at the given path', () => { - const object: any = {}; - set(object, 'test', 1); - expect(object.test).toBe(1); + describe('Correctly setting the value at the given path', () => { + it('sets the value for a simple path for both string and array paths', () => { + const object = {}; + let settedObject = set(object, 'test', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({ test: 1 }); + + settedObject = set(object, ['test2'], 1); + expect(object).toBe(settedObject); + expect(object).toEqual({ test: 1, test2: 1 }); + }); + + it('sets the value for complex string and array paths (without indexes)', () => { + const object = {}; + const object2 = {}; + + // the given paths are equivalent in string vs array format + _set(object, 'a.b.c', 'c'); + _set(object2, ['a', 'b', 'c'], 'c'); + expect(object).toEqual({ a: { b: { c: 'c' } } }); + expect(object2).toEqual(object); + + _set(object, 'a.b.d', 'd'); + _set(object2, ['a', 'b', 'd'], 'd'); + expect(object).toEqual({ + a: { b: { c: 'c', d: 'd' } }, + }); + expect(object2).toEqual(object); + + _set(object, 'e[f][g]', 'g'); + _set(object2, ['e', 'f', 'g'], 'g'); + expect(object).toEqual({ + a: { b: { c: 'c', d: 'd' } }, + e: { f: { g: 'g' } }, + }); + expect(object2).toEqual(object); + }); + + it('sets the value for complex string and array paths (with indexes)', () => { + const object = {}; + const object2 = {}; + + // the given paths are equivalent in string vs array format + _set(object, 'a.b.1', 'b1'); + _set(object2, ['a', 'b', '1'], 'b1'); + expect(object).toEqual({ a: { b: [undefined, 'b1'] } }); + expect(object2).toEqual(object); + + // If path is an array, indexes can be passed in as a string or as a number + _set(object, 'a.b.2', 'b2'); + _set(object2, ['a', 'b', 2], 'b2'); + expect(object).toEqual({ + a: { b: [undefined, 'b1', 'b2'] }, + }); + expect(object2).toEqual(object); + + _set(object, 'a.b.3.c', 'c'); + _set(object2, ['a', 'b', 3, 'c'], 'c'); + expect(object).toEqual({ + a: { b: [undefined, 'b1', 'b2', { c: 'c' }] }, + }); + expect(object2).toEqual(object); + }); + + it('creates an empty key', () => { + expect(_set({}, '', 'empty string')).toEqual({ '': 'empty string' }); + expect(set({}, [''], 'empty string for array')).toEqual({ + '': 'empty string for array', + }); + }); + + it('creates an undefined key', () => { + const undefinedKey = {}; + _set(undefinedKey, [], 'undefined'); + expect(undefinedKey).toEqual({}); + }); + + it('only considers valid indexes for setting array values', () => { + const object = {}; + + expect(_set(object, 'test[01].test1', 'test')).toEqual({ + test: { '01': { test1: 'test' } }, + }); + expect(_set(object, 'test[01].[02]', 'test2')).toEqual({ + test: { '01': { '02': 'test2', test1: 'test' } }, + }); + expect(_set(object, 'test[-01]', 'test3')).toEqual({ + test: { '-01': 'test3', '01': { '02': 'test2', test1: 'test' } }, + }); + }); + + it('considers numbers as keys if they are not used as indexes or if there is an already existing object', () => { + const object = {}; + set(object, 1, 'test'); + expect(object).toEqual({ 1: 'test' }); + + set(object, [2], '2'); + expect(object).toEqual({ 1: 'test', 2: '2' }); + + expect(_set({ test: { test1: 'test' } }, 'test.1', 'test2')).toEqual({ + test: { '1': 'test2', test1: 'test' }, + }); + }); + + it('treats later numbers as indexes (if they are valid indexes)', () => { + const obj1 = _set({}, '1[1]', 'test'); + expect(obj1).toEqual({ 1: [undefined, 'test'] }); + + const obj2 = _set({}, '1.2', 'test'); + expect(obj2).toEqual({ 1: [undefined, undefined, 'test'] }); + + const obj3 = _set({}, [3, 1], 'test'); + expect(obj3).toEqual({ 3: [undefined, 'test'] }); + + const obj4 = _set({}, [1, 1, 2], 'test'); + expect(obj4).toEqual({ 1: [undefined, [undefined, undefined, 'test']] }); + }); + + it('can uses symbols as keys', () => { + // do these count as symbols or strings here though... I was trying '\@', but eslint changed it to below + const object = {}; + expect(set(object, '%', 'test')).toEqual({ '%': 'test' }); + expect(set(object, ['@'], '2')).toEqual({ '%': 'test', '@': '2' }); + }); + + it('sets the value at an already existing key', () => { + const alreadyExisting = { test: 'test' }; + expect(set(alreadyExisting, 'test', 'changed')).toEqual({ + test: 'changed', + }); + expect(set(alreadyExisting, ['test'], 'changed again')).toEqual({ + test: 'changed again', + }); + expect(_set(alreadyExisting, ['test', 'test2'], 'changed x3')).toEqual({ + test: { test2: 'changed x3' }, + }); + expect(_set(alreadyExisting, 'test[test2][test3]', 'changed x4')).toEqual( + { + test: { test2: { test3: 'changed x4' } }, + } + ); + }); + }); + + describe('Ensuring safety against prototype pollution and that the passed in and returned object are the same', () => { + it('protects against the given string path matching a prototype pollution key', () => { + const object = {}; + // __proto__ + let settedObject = set(object, '__proto__', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + // constructor + settedObject = set(object, 'constructor', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + // prototype + settedObject = set(object, 'prototype', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + }); + + it('protects against the given string path containing prototype pollution keys that are separated by path delimiters', () => { + const object = {}; + // prototype pollution key separated by . + let settedObject = set(object, 'test.__proto__.test', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + settedObject = set(object, 'test.constructor.test', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + settedObject = set(object, 'test.prototype.test', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + // prototype pollution key separated by [] + settedObject = set(object, 'test.test[__proto__]', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + settedObject = set(object, 'test.test[constructor]', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + settedObject = set(object, 'test.test[prototype]', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + }); + + it('checks for prototype pollution keys in array paths', () => { + const object = {}; + let settedObject = set(object, [1, 2, '__proto__'], 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + settedObject = set(object, ['constructor', 'test', 'test'], 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + settedObject = set(object, ['test', 'prototype', 'test', 3], 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + }); + + it('is not considered prototype pollution if the string paths have a key not separated by delimiters', () => { + const object = {}; + // prototype pollution key separated by . + let settedObject = set(object, 'test__proto__test', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({ test__proto__test: 1 }); + + settedObject = set(object, 'constructortest', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({ constructortest: 1, test__proto__test: 1 }); + + settedObject = set(object, 'testprototype', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({ + constructortest: 1, + test__proto__test: 1, + testprototype: 1, + }); + }); + + it('is not considered prototype pollution if array paths do not have standalone prototype pollution keys', () => { + const object = {}; + // prototype pollution key separated by . + let settedObject = set(object, ['test__proto__test'], 1); + expect(object).toBe(settedObject); + expect(object).toEqual({ test__proto__test: 1 }); + + settedObject = set(object, ['__proto__.test'], 1); + expect(object).toBe(settedObject); + expect(object).toEqual({ '__proto__.test': 1, test__proto__test: 1 }); + + // this will fail -- array paths not working yet + settedObject = set(object, ['constructortest', 'test'], 1); + // expect(object).toBe(settedObject); + // expect(object).toEqual({ + // constructortest: { test: 1 }, + // test__proto__test: 1, + // }); + + // settedObject = set(object, ['testprototype'], 1); + // expect(object).toBe(settedObject); + // expect(object).toEqual({ + // constructortest: { test: 1 }, + // test__proto__test: 1, + // testprototype: 1, + // }); + }); }); }); -describe('Tests for isValuePrototypePollutionSafe', () => { +describe('Tests for isKeyPrototypePollutionSafe', () => { it('determines that given array is prototype pollution safe', () => { - // expect(isValuePrototypePollutionSafe([])).toBe(true); - // expect(isValuePrototypePollutionSafe(['', 'test'])).toBe(true); - // expect(isValuePrototypePollutionSafe(['', 1])).toBe(true); - // expect(isValuePrototypePollutionSafe(['', {}, { test: 'test' }, 3])).toBe( - // true - // ); + expect(isKeyPrototypePollutionSafe([])).toBe(true); + expect(isKeyPrototypePollutionSafe(['', 'test'])).toBe(true); + expect(isKeyPrototypePollutionSafe(['', 1])).toBe(true); }); + it('determines that the given array is not prototype pollution safe', () => { - expect(isValuePrototypePollutionSafe(['__proto__'])).toBe(false); - expect(isValuePrototypePollutionSafe(['', 'test', 'prototype'])).toBe( - false - ); - expect(isValuePrototypePollutionSafe(['', 1, 'constructor'])).toBe(false); - expect( - isValuePrototypePollutionSafe(['', 1, 'constructor', '__proto__']) - ).toBe(false); - // expect(isValuePrototypePollutionSafe(['', 1, { __proto__: 'test' }])).toBe( - // false - // ); + expect(isKeyPrototypePollutionSafe(['__proto__'])).toBe(false); + expect(isKeyPrototypePollutionSafe(['', 'test', 'prototype'])).toBe(false); + expect(isKeyPrototypePollutionSafe(['', 1, 'constructor'])).toBe(false); expect( - isValuePrototypePollutionSafe(['', 1, { constructor: 'test' }]) + isKeyPrototypePollutionSafe(['', 1, 'constructor', '__proto__']) ).toBe(false); - expect(isValuePrototypePollutionSafe(['', 1, { prototype: 'test' }])).toBe( - false - ); - // expect( - // isValuePrototypePollutionSafe([{ test: { test: { __proto__: 'test' } } }]) - // ).toBe(false); - // sadcat moment }); }); diff --git a/packages/manager/src/utilities/set.ts b/packages/manager/src/utilities/set.ts index c5594bf215e..3e16089235b 100644 --- a/packages/manager/src/utilities/set.ts +++ b/packages/manager/src/utilities/set.ts @@ -22,14 +22,11 @@ export function set( } // if the path is not an array, convert it to an array format - const updatedPath: PropertyName[] = Array.isArray(path) - ? path - : path.toString().match(/[^.[\]]+/g) ?? []; + const updatedPath = determinePath(path); if ( // ensure that both the path and value will not lead to prototype pollution issues - !isValuePrototypePollutionSafe(updatedPath) || - !isValuePrototypePollutionSafe(value) + !isKeyPrototypePollutionSafe(updatedPath) ) { return object; } @@ -58,7 +55,7 @@ export function set( * @param value - The value to check * @return - If value is safe, returns it; otherwise returns undefined */ -export const isValuePrototypePollutionSafe = (value: any): boolean => { +export const isKeyPrototypePollutionSafe = (value: PropertyPath): boolean => { if (typeof value === 'string') { return ( value !== '__proto__' && value !== 'prototype' && value !== 'constructor' @@ -68,19 +65,15 @@ export const isValuePrototypePollutionSafe = (value: any): boolean => { // An array is safe if all of its value are safe if (Array.isArray(value) && value.length > 0) { return ( - isValuePrototypePollutionSafe(value[0]) && - isValuePrototypePollutionSafe(value.splice(1)) - ); - } - - // An object (that is not an array) is safe if all of its values and keys are safe - if (value && !Array.isArray(value) && typeof value === 'object') { - return ( - isValuePrototypePollutionSafe(Object.keys(value)) && - isValuePrototypePollutionSafe(Object.values(value)) + isKeyPrototypePollutionSafe(value[0]) && + isKeyPrototypePollutionSafe(value.splice(1)) ); } // If the value we are checking is not an array/object, we assume it to be safe return true; }; + +export const determinePath = (path: PropertyPath): PropertyName[] => { + return Array.isArray(path) ? path : path.toString().match(/[^.[\]]+/g) ?? []; +}; From a252d41c464bbfceaaaa546c7cbb1684151ae991 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 21 Aug 2024 17:23:49 -0400 Subject: [PATCH 07/17] determine if supposed index is actually a valid index --- packages/manager/src/utilities/set.test.ts | 15 +++++++++++ packages/manager/src/utilities/set.ts | 30 +++++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/utilities/set.test.ts b/packages/manager/src/utilities/set.test.ts index 5e4f70ff046..97e09f326cc 100644 --- a/packages/manager/src/utilities/set.test.ts +++ b/packages/manager/src/utilities/set.test.ts @@ -94,6 +94,21 @@ describe('Tests for set', () => { expect(_set(object, 'test[-01]', 'test3')).toEqual({ test: { '-01': 'test3', '01': { '02': 'test2', test1: 'test' } }, }); + expect(_set(object, 'test[ 02]', 'test4')).toEqual({ + test: { + ' 02': 'test4', + '-01': 'test3', + '01': { '02': 'test2', test1: 'test' }, + }, + }); + expect(_set(object, 'test[00]', 'test5')).toEqual({ + test: { + ' 02': 'test4', + '-01': 'test3', + '00': 'test5', + '01': { '02': 'test2', test1: 'test' }, + }, + }); }); it('considers numbers as keys if they are not used as indexes or if there is an already existing object', () => { diff --git a/packages/manager/src/utilities/set.ts b/packages/manager/src/utilities/set.ts index 3e16089235b..b2c36dd7ad9 100644 --- a/packages/manager/src/utilities/set.ts +++ b/packages/manager/src/utilities/set.ts @@ -36,8 +36,7 @@ export function set( const key = updatedPath[i]; if (!updatingObject[key] || typeof updatingObject[key] !== 'object') { const nextKey = updatedPath[i + 1]; - // this line has to be changed, because "01" should lead to an object's keys, not an array - updatingObject[key] = Number.isNaN(nextKey) ? {} : []; + updatingObject[key] = isValidIndex(nextKey) ? [] : {}; } updatingObject = updatingObject[key]; @@ -74,6 +73,31 @@ export const isKeyPrototypePollutionSafe = (value: PropertyPath): boolean => { return true; }; -export const determinePath = (path: PropertyPath): PropertyName[] => { +/** + * Determines the path to set some value at, converting any string paths + * into array format. + * + * @param path - The path to check + * @returns - The path to set a value, in array format + */ +const determinePath = (path: PropertyPath): PropertyName[] => { return Array.isArray(path) ? path : path.toString().match(/[^.[\]]+/g) ?? []; }; + +/** + * Determines if the given value can be considered a valid index for an array. + * For example, 0, 1, 2 are all valid index. + * -1, 01, -01, 00, '1 1' are not. + * + * @param value - The value to check + * + * @returns - Returns boolean + */ +const isValidIndex = (value: PropertyName) => { + const convertedValue = value.toString(); + + return ( + /^\d+$/.test(convertedValue) && // must be all digits + (convertedValue.length < 2 || !convertedValue.startsWith('0')) // must not start with 0 (unless it is 0 only) + ); +}; From 3d52159e30d2479210c61e07c2b79e2b5270e788 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 21 Aug 2024 23:35:12 -0400 Subject: [PATCH 08/17] some path indexing that seems difficult to pursue --- packages/manager/src/utilities/set.test.ts | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/manager/src/utilities/set.test.ts b/packages/manager/src/utilities/set.test.ts index 97e09f326cc..0e32acda54a 100644 --- a/packages/manager/src/utilities/set.test.ts +++ b/packages/manager/src/utilities/set.test.ts @@ -162,6 +162,37 @@ describe('Tests for set', () => { } ); }); + + // it will take an incredible amount of effort to get set to this level of finesse + it.skip('sets the value for nonstandard paths', () => { + expect(_set({}, 'test..test', 'testing')).toEqual({ + test: { '': { test: 'testing' } }, + }); + expect(_set({}, 'test.[.test]', 'testing 2')).toEqual({ + test: { test: 'testing 2' }, + }); + expect(_set({}, 'test.[te[st]', 'testing 3')).toEqual({ + test: { te: { st: 'testing 3' } }, + }); + expect(_set({}, 'test.]test', 'testing 4')).toEqual({ + test: { test: 'testing 4' }, + }); + expect(_set({}, 'test.[]', 'testing 5')).toEqual({ + test: { '': { '': 'testing 5' } }, + }); + expect(_set({}, '[].test', 'testing 6')).toEqual({ + '': { test: 'testing 6' }, + }); + expect(_set({}, '.', 'testing 7')).toEqual({ + '': { '': 'testing 7' }, + }); + expect(_set({}, '[', 'testing 8')).toEqual({ + '[': 'testing 8', + }); + expect(_set({}, ']', 'testing 9')).toEqual({ + ']': 'testing 9', + }); + }); }); describe('Ensuring safety against prototype pollution and that the passed in and returned object are the same', () => { From 8ed1033e0cb93ac2fe8b1dcf7d6dbbeb02261cd2 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 22 Aug 2024 10:08:01 -0400 Subject: [PATCH 09/17] update naming, tests --- packages/manager/src/utilities/set.test.ts | 24 ++++++++-------- packages/manager/src/utilities/set.ts | 32 ++++++++-------------- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/packages/manager/src/utilities/set.test.ts b/packages/manager/src/utilities/set.test.ts index 0e32acda54a..2506734bf49 100644 --- a/packages/manager/src/utilities/set.test.ts +++ b/packages/manager/src/utilities/set.test.ts @@ -1,6 +1,6 @@ import _set from 'lodash.set'; -import { isKeyPrototypePollutionSafe, set } from './set'; +import { isPrototypePollutionSafe, set } from './set'; // todo: as i debug get rid of calls to _set @@ -308,21 +308,19 @@ describe('Tests for set', () => { }); }); -describe('Tests for isKeyPrototypePollutionSafe', () => { +describe('Tests for isPrototypePollutionSafe', () => { it('determines that given array is prototype pollution safe', () => { - expect(isKeyPrototypePollutionSafe([])).toBe(true); - expect(isKeyPrototypePollutionSafe(['', 'test'])).toBe(true); - expect(isKeyPrototypePollutionSafe(['', 1])).toBe(true); + expect(isPrototypePollutionSafe([])).toBe(true); + expect(isPrototypePollutionSafe(['', 'test'])).toBe(true); + expect(isPrototypePollutionSafe(['', 1])).toBe(true); }); it('determines that the given array is not prototype pollution safe', () => { - expect(isKeyPrototypePollutionSafe(['__proto__'])).toBe(false); - expect(isKeyPrototypePollutionSafe(['', 'test', 'prototype'])).toBe(false); - expect(isKeyPrototypePollutionSafe(['', 1, 'constructor'])).toBe(false); - expect( - isKeyPrototypePollutionSafe(['', 1, 'constructor', '__proto__']) - ).toBe(false); + expect(isPrototypePollutionSafe(['__proto__'])).toBe(false); + expect(isPrototypePollutionSafe(['', 'test', 'prototype'])).toBe(false); + expect(isPrototypePollutionSafe(['', 1, 'constructor'])).toBe(false); + expect(isPrototypePollutionSafe(['', 1, 'constructor', '__proto__'])).toBe( + false + ); }); }); - -// there will definitely be (A LOT) more tests diff --git a/packages/manager/src/utilities/set.ts b/packages/manager/src/utilities/set.ts index b2c36dd7ad9..f7735e6a6cf 100644 --- a/packages/manager/src/utilities/set.ts +++ b/packages/manager/src/utilities/set.ts @@ -26,7 +26,7 @@ export function set( if ( // ensure that both the path and value will not lead to prototype pollution issues - !isKeyPrototypePollutionSafe(updatedPath) + !isPrototypePollutionSafe(updatedPath) ) { return object; } @@ -49,28 +49,20 @@ export function set( } /** - * Helper to ensure a value cannot lead to a prototype pollution issue. + * Helper to ensure a path cannot lead to a prototype pollution issue. * - * @param value - The value to check + * @param path - The path to check * @return - If value is safe, returns it; otherwise returns undefined */ -export const isKeyPrototypePollutionSafe = (value: PropertyPath): boolean => { - if (typeof value === 'string') { - return ( - value !== '__proto__' && value !== 'prototype' && value !== 'constructor' - ); - } - - // An array is safe if all of its value are safe - if (Array.isArray(value) && value.length > 0) { - return ( - isKeyPrototypePollutionSafe(value[0]) && - isKeyPrototypePollutionSafe(value.splice(1)) - ); - } - - // If the value we are checking is not an array/object, we assume it to be safe - return true; +export const isPrototypePollutionSafe = (path: PropertyName[]): boolean => { + return path.reduce((safeSoFar, val) => { + let isCurKeySafe = true; + if (typeof val === 'string') { + isCurKeySafe = + val !== '__proto__' && val !== 'prototype' && val !== 'constructor'; + } + return safeSoFar && isCurKeySafe; + }, true); }; /** From 46893dfae089ed0d3a8dd68fa233746f2ce974dd Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 22 Aug 2024 12:24:55 -0400 Subject: [PATCH 10/17] remove set from package.json --- packages/manager/package.json | 2 - .../manager/src/utilities/formikErrorUtils.ts | 2 +- packages/manager/src/utilities/set.test.ts | 93 +++++++++---------- packages/manager/src/utilities/set.ts | 24 +++-- yarn.lock | 12 --- 5 files changed, 60 insertions(+), 73 deletions(-) diff --git a/packages/manager/package.json b/packages/manager/package.json index 2ac52547032..e15950c70b6 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -49,7 +49,6 @@ "libphonenumber-js": "^1.10.6", "lodash.clonedeep": "^4.5.0", "lodash.curry": "^4.1.1", - "lodash.set": "^4.3.2", "logic-query-parser": "^0.0.5", "luxon": "3.4.4", "markdown-it": "^12.3.2", @@ -143,7 +142,6 @@ "@types/jspdf": "^1.3.3", "@types/lodash.clonedeep": "^4.5.9", "@types/lodash.curry": "^4.1.9", - "@types/lodash.set": "^4.3.9", "@types/luxon": "3.4.2", "@types/markdown-it": "^10.0.2", "@types/md5": "^2.1.32", diff --git a/packages/manager/src/utilities/formikErrorUtils.ts b/packages/manager/src/utilities/formikErrorUtils.ts index 3e4e14deea4..b17047942fe 100644 --- a/packages/manager/src/utilities/formikErrorUtils.ts +++ b/packages/manager/src/utilities/formikErrorUtils.ts @@ -1,8 +1,8 @@ -import set from 'lodash.set'; import { reverse } from 'ramda'; import { getAPIErrorOrDefault } from './errorUtils'; import { isNilOrEmpty } from './isNilOrEmpty'; +import { set } from './set'; import type { APIError } from '@linode/api-v4/lib/types'; import type { FormikErrors } from 'formik'; diff --git a/packages/manager/src/utilities/set.test.ts b/packages/manager/src/utilities/set.test.ts index 2506734bf49..0a8fa82ceff 100644 --- a/packages/manager/src/utilities/set.test.ts +++ b/packages/manager/src/utilities/set.test.ts @@ -1,5 +1,3 @@ -import _set from 'lodash.set'; - import { isPrototypePollutionSafe, set } from './set'; // todo: as i debug get rid of calls to _set @@ -22,20 +20,20 @@ describe('Tests for set', () => { const object2 = {}; // the given paths are equivalent in string vs array format - _set(object, 'a.b.c', 'c'); - _set(object2, ['a', 'b', 'c'], 'c'); + set(object, 'a.b.c', 'c'); + set(object2, ['a', 'b', 'c'], 'c'); expect(object).toEqual({ a: { b: { c: 'c' } } }); expect(object2).toEqual(object); - _set(object, 'a.b.d', 'd'); - _set(object2, ['a', 'b', 'd'], 'd'); + set(object, 'a.b.d', 'd'); + set(object2, ['a', 'b', 'd'], 'd'); expect(object).toEqual({ a: { b: { c: 'c', d: 'd' } }, }); expect(object2).toEqual(object); - _set(object, 'e[f][g]', 'g'); - _set(object2, ['e', 'f', 'g'], 'g'); + set(object, 'e[f][g]', 'g'); + set(object2, ['e', 'f', 'g'], 'g'); expect(object).toEqual({ a: { b: { c: 'c', d: 'd' } }, e: { f: { g: 'g' } }, @@ -48,21 +46,21 @@ describe('Tests for set', () => { const object2 = {}; // the given paths are equivalent in string vs array format - _set(object, 'a.b.1', 'b1'); - _set(object2, ['a', 'b', '1'], 'b1'); + set(object, 'a.b.1', 'b1'); + set(object2, ['a', 'b', '1'], 'b1'); expect(object).toEqual({ a: { b: [undefined, 'b1'] } }); expect(object2).toEqual(object); // If path is an array, indexes can be passed in as a string or as a number - _set(object, 'a.b.2', 'b2'); - _set(object2, ['a', 'b', 2], 'b2'); + set(object, 'a.b.2', 'b2'); + set(object2, ['a', 'b', 2], 'b2'); expect(object).toEqual({ a: { b: [undefined, 'b1', 'b2'] }, }); expect(object2).toEqual(object); - _set(object, 'a.b.3.c', 'c'); - _set(object2, ['a', 'b', 3, 'c'], 'c'); + set(object, 'a.b.3.c', 'c'); + set(object2, ['a', 'b', 3, 'c'], 'c'); expect(object).toEqual({ a: { b: [undefined, 'b1', 'b2', { c: 'c' }] }, }); @@ -70,38 +68,36 @@ describe('Tests for set', () => { }); it('creates an empty key', () => { - expect(_set({}, '', 'empty string')).toEqual({ '': 'empty string' }); + expect(set({}, '', 'empty string')).toEqual({ '': 'empty string' }); expect(set({}, [''], 'empty string for array')).toEqual({ '': 'empty string for array', }); }); - it('creates an undefined key', () => { - const undefinedKey = {}; - _set(undefinedKey, [], 'undefined'); - expect(undefinedKey).toEqual({}); + it('does not set anything if no path is given', () => { + expect(set({}, [], 'should not set')).toEqual({}); }); it('only considers valid indexes for setting array values', () => { const object = {}; - expect(_set(object, 'test[01].test1', 'test')).toEqual({ + expect(set(object, 'test[01].test1', 'test')).toEqual({ test: { '01': { test1: 'test' } }, }); - expect(_set(object, 'test[01].[02]', 'test2')).toEqual({ + expect(set(object, 'test[01].[02]', 'test2')).toEqual({ test: { '01': { '02': 'test2', test1: 'test' } }, }); - expect(_set(object, 'test[-01]', 'test3')).toEqual({ + expect(set(object, 'test[-01]', 'test3')).toEqual({ test: { '-01': 'test3', '01': { '02': 'test2', test1: 'test' } }, }); - expect(_set(object, 'test[ 02]', 'test4')).toEqual({ + expect(set(object, 'test[ 02]', 'test4')).toEqual({ test: { ' 02': 'test4', '-01': 'test3', '01': { '02': 'test2', test1: 'test' }, }, }); - expect(_set(object, 'test[00]', 'test5')).toEqual({ + expect(set(object, 'test[00]', 'test5')).toEqual({ test: { ' 02': 'test4', '-01': 'test3', @@ -119,22 +115,22 @@ describe('Tests for set', () => { set(object, [2], '2'); expect(object).toEqual({ 1: 'test', 2: '2' }); - expect(_set({ test: { test1: 'test' } }, 'test.1', 'test2')).toEqual({ + expect(set({ test: { test1: 'test' } }, 'test.1', 'test2')).toEqual({ test: { '1': 'test2', test1: 'test' }, }); }); it('treats later numbers as indexes (if they are valid indexes)', () => { - const obj1 = _set({}, '1[1]', 'test'); + const obj1 = set({}, '1[1]', 'test'); expect(obj1).toEqual({ 1: [undefined, 'test'] }); - const obj2 = _set({}, '1.2', 'test'); + const obj2 = set({}, '1.2', 'test'); expect(obj2).toEqual({ 1: [undefined, undefined, 'test'] }); - const obj3 = _set({}, [3, 1], 'test'); + const obj3 = set({}, [3, 1], 'test'); expect(obj3).toEqual({ 3: [undefined, 'test'] }); - const obj4 = _set({}, [1, 1, 2], 'test'); + const obj4 = set({}, [1, 1, 2], 'test'); expect(obj4).toEqual({ 1: [undefined, [undefined, undefined, 'test']] }); }); @@ -153,45 +149,46 @@ describe('Tests for set', () => { expect(set(alreadyExisting, ['test'], 'changed again')).toEqual({ test: 'changed again', }); - expect(_set(alreadyExisting, ['test', 'test2'], 'changed x3')).toEqual({ + expect(set(alreadyExisting, ['test', 'test2'], 'changed x3')).toEqual({ test: { test2: 'changed x3' }, }); - expect(_set(alreadyExisting, 'test[test2][test3]', 'changed x4')).toEqual( - { - test: { test2: { test3: 'changed x4' } }, - } - ); + expect(set(alreadyExisting, 'test[test2][test3]', 'changed x4')).toEqual({ + test: { test2: { test3: 'changed x4' } }, + }); }); - // it will take an incredible amount of effort to get set to this level of finesse - it.skip('sets the value for nonstandard paths', () => { - expect(_set({}, 'test..test', 'testing')).toEqual({ - test: { '': { test: 'testing' } }, - }); - expect(_set({}, 'test.[.test]', 'testing 2')).toEqual({ + it('sets the value for nonstandard paths', () => { + expect(set({}, 'test.[.test]', 'testing 2')).toEqual({ test: { test: 'testing 2' }, }); - expect(_set({}, 'test.[te[st]', 'testing 3')).toEqual({ + expect(set({}, 'test.[te[st]', 'testing 3')).toEqual({ test: { te: { st: 'testing 3' } }, }); - expect(_set({}, 'test.]test', 'testing 4')).toEqual({ + expect(set({}, 'test.]test', 'testing 4')).toEqual({ test: { test: 'testing 4' }, }); - expect(_set({}, 'test.[]', 'testing 5')).toEqual({ + }); + + // it will take an incredible amount of effort to get set to this level of finesse + it.skip('sets the value for non standard paths - not working currently', () => { + expect(set({}, 'test.[]', 'testing 5')).toEqual({ test: { '': { '': 'testing 5' } }, }); - expect(_set({}, '[].test', 'testing 6')).toEqual({ + expect(set({}, '[].test', 'testing 6')).toEqual({ '': { test: 'testing 6' }, }); - expect(_set({}, '.', 'testing 7')).toEqual({ + expect(set({}, '.', 'testing 7')).toEqual({ '': { '': 'testing 7' }, }); - expect(_set({}, '[', 'testing 8')).toEqual({ + expect(set({}, '[', 'testing 8')).toEqual({ '[': 'testing 8', }); - expect(_set({}, ']', 'testing 9')).toEqual({ + expect(set({}, ']', 'testing 9')).toEqual({ ']': 'testing 9', }); + expect(set({}, 'test..test', 'testing')).toEqual({ + test: { '': { test: 'testing' } }, + }); }); }); diff --git a/packages/manager/src/utilities/set.ts b/packages/manager/src/utilities/set.ts index f7735e6a6cf..4718b201f5e 100644 --- a/packages/manager/src/utilities/set.ts +++ b/packages/manager/src/utilities/set.ts @@ -3,8 +3,9 @@ type PropertyPath = PropertyName | readonly PropertyName[]; /** * Helper to set the given value at the given path of the given object. - * This util is a direct replacement for `set` from lodash, with additional - * checks to protect against prototype pollution. + * This util serves as a replacement for `set` from lodash, with additional checks + * to protect against prototype pollution. It is not (yet) a perfect replacement (see + * skipped test examples in set.test.ts), but does handle current CM use cases. * * @param object — The object to modify. * @param path — The path of the property to set. @@ -16,7 +17,7 @@ export function set( path: PropertyPath, value: any ): T { - // ensure that object is actually an object + // ensure that object is actually a non-array object if (!object || Array.isArray(object) || typeof object !== 'object') { return object; } @@ -26,7 +27,9 @@ export function set( if ( // ensure that both the path and value will not lead to prototype pollution issues - !isPrototypePollutionSafe(updatedPath) + // and that there is actually a path to set + !isPrototypePollutionSafe(updatedPath) || + updatedPath.length === 0 ) { return object; } @@ -56,11 +59,10 @@ export function set( */ export const isPrototypePollutionSafe = (path: PropertyName[]): boolean => { return path.reduce((safeSoFar, val) => { - let isCurKeySafe = true; - if (typeof val === 'string') { - isCurKeySafe = - val !== '__proto__' && val !== 'prototype' && val !== 'constructor'; - } + const isCurKeySafe = + typeof val === 'string' + ? val !== '__proto__' && val !== 'prototype' && val !== 'constructor' + : true; return safeSoFar && isCurKeySafe; }, true); }; @@ -73,7 +75,9 @@ export const isPrototypePollutionSafe = (path: PropertyName[]): boolean => { * @returns - The path to set a value, in array format */ const determinePath = (path: PropertyPath): PropertyName[] => { - return Array.isArray(path) ? path : path.toString().match(/[^.[\]]+/g) ?? []; + return Array.isArray(path) + ? path + : path.toString().match(/[^.[\]]+/g) ?? ['']; }; /** diff --git a/yarn.lock b/yarn.lock index db0eb12fd97..f2eb8bb304d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4147,13 +4147,6 @@ dependencies: "@types/lodash" "*" -"@types/lodash.set@^4.3.9": - version "4.3.9" - resolved "https://registry.yarnpkg.com/@types/lodash.set/-/lodash.set-4.3.9.tgz#55d95bce407b42c6655f29b2d0811fd428e698f0" - integrity sha512-KOxyNkZpbaggVmqbpr82N2tDVTx05/3/j0f50Es1prxrWB0XYf9p3QNxqcbWb7P1Q9wlvsUSlCFnwlPCIJ46PQ== - dependencies: - "@types/lodash" "*" - "@types/lodash@*": version "4.17.0" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.0.tgz#d774355e41f372d5350a4d0714abb48194a489c3" @@ -9681,11 +9674,6 @@ lodash.once@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== -lodash.set@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" - integrity sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg== - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" From ce4bfbcd95e3f331b1d46cc917c4225ecd4d1604 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 22 Aug 2024 12:28:43 -0400 Subject: [PATCH 11/17] add back in some commented out tests --- packages/manager/src/utilities/set.test.ts | 31 +++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/manager/src/utilities/set.test.ts b/packages/manager/src/utilities/set.test.ts index 0a8fa82ceff..b364c06b187 100644 --- a/packages/manager/src/utilities/set.test.ts +++ b/packages/manager/src/utilities/set.test.ts @@ -1,7 +1,5 @@ import { isPrototypePollutionSafe, set } from './set'; -// todo: as i debug get rid of calls to _set - describe('Tests for set', () => { describe('Correctly setting the value at the given path', () => { it('sets the value for a simple path for both string and array paths', () => { @@ -286,21 +284,22 @@ describe('Tests for set', () => { expect(object).toBe(settedObject); expect(object).toEqual({ '__proto__.test': 1, test__proto__test: 1 }); - // this will fail -- array paths not working yet settedObject = set(object, ['constructortest', 'test'], 1); - // expect(object).toBe(settedObject); - // expect(object).toEqual({ - // constructortest: { test: 1 }, - // test__proto__test: 1, - // }); - - // settedObject = set(object, ['testprototype'], 1); - // expect(object).toBe(settedObject); - // expect(object).toEqual({ - // constructortest: { test: 1 }, - // test__proto__test: 1, - // testprototype: 1, - // }); + expect(object).toBe(settedObject); + expect(object).toEqual({ + '__proto__.test': 1, + constructortest: { test: 1 }, + test__proto__test: 1, + }); + + settedObject = set(object, ['testprototype'], 1); + expect(object).toBe(settedObject); + expect(object).toEqual({ + '__proto__.test': 1, + constructortest: { test: 1 }, + test__proto__test: 1, + testprototype: 1, + }); }); }); }); From 559e49d225942eb56651c2bce9a334d202933102 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 22 Aug 2024 12:49:13 -0400 Subject: [PATCH 12/17] some cleanup, need to look over everything --- packages/manager/src/utilities/set.test.ts | 9 ++++++--- packages/manager/src/utilities/set.ts | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/utilities/set.test.ts b/packages/manager/src/utilities/set.test.ts index b364c06b187..884321895b7 100644 --- a/packages/manager/src/utilities/set.test.ts +++ b/packages/manager/src/utilities/set.test.ts @@ -48,19 +48,23 @@ describe('Tests for set', () => { set(object2, ['a', 'b', '1'], 'b1'); expect(object).toEqual({ a: { b: [undefined, 'b1'] } }); expect(object2).toEqual(object); + set(object, 'a.b.0', 5); + set(object2, ['a', 'b', 0], 5); + expect(object).toEqual({ a: { b: [5, 'b1'] } }); + expect(object2).toEqual(object); // If path is an array, indexes can be passed in as a string or as a number set(object, 'a.b.2', 'b2'); set(object2, ['a', 'b', 2], 'b2'); expect(object).toEqual({ - a: { b: [undefined, 'b1', 'b2'] }, + a: { b: [5, 'b1', 'b2'] }, }); expect(object2).toEqual(object); set(object, 'a.b.3.c', 'c'); set(object2, ['a', 'b', 3, 'c'], 'c'); expect(object).toEqual({ - a: { b: [undefined, 'b1', 'b2', { c: 'c' }] }, + a: { b: [5, 'b1', 'b2', { c: 'c' }] }, }); expect(object2).toEqual(object); }); @@ -275,7 +279,6 @@ describe('Tests for set', () => { it('is not considered prototype pollution if array paths do not have standalone prototype pollution keys', () => { const object = {}; - // prototype pollution key separated by . let settedObject = set(object, ['test__proto__test'], 1); expect(object).toBe(settedObject); expect(object).toEqual({ test__proto__test: 1 }); diff --git a/packages/manager/src/utilities/set.ts b/packages/manager/src/utilities/set.ts index 4718b201f5e..62d2ec2785a 100644 --- a/packages/manager/src/utilities/set.ts +++ b/packages/manager/src/utilities/set.ts @@ -35,8 +35,10 @@ export function set( } let updatingObject: any = object; + // if we haven't reached the last key, traverse through object, for (let i = 0; i < updatedPath.length - 1; i++) { const key = updatedPath[i]; + // we set object[key] to be [] or {} depending on the next key if (!updatingObject[key] || typeof updatingObject[key] !== 'object') { const nextKey = updatedPath[i + 1]; updatingObject[key] = isValidIndex(nextKey) ? [] : {}; @@ -82,7 +84,7 @@ const determinePath = (path: PropertyPath): PropertyName[] => { /** * Determines if the given value can be considered a valid index for an array. - * For example, 0, 1, 2 are all valid index. + * For example, 0, 1, 2 are all valid indexes. * -1, 01, -01, 00, '1 1' are not. * * @param value - The value to check From be10ebe8cc6475db4d219554f5ff48efa8ed6fae Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 22 Aug 2024 17:00:03 -0400 Subject: [PATCH 13/17] update test cases and comments --- packages/manager/src/utilities/set.test.ts | 26 +++++++++++++++------- packages/manager/src/utilities/set.ts | 26 +++++++++++++++------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/packages/manager/src/utilities/set.test.ts b/packages/manager/src/utilities/set.test.ts index 884321895b7..e6b6157e72b 100644 --- a/packages/manager/src/utilities/set.test.ts +++ b/packages/manager/src/utilities/set.test.ts @@ -1,6 +1,10 @@ import { isPrototypePollutionSafe, set } from './set'; describe('Tests for set', () => { + it("returns the passed in 'object' as is if it's not actually a (non array) object", () => { + expect(set([], 'path not needed', 3)).toEqual([]); + }); + describe('Correctly setting the value at the given path', () => { it('sets the value for a simple path for both string and array paths', () => { const object = {}; @@ -48,7 +52,7 @@ describe('Tests for set', () => { set(object2, ['a', 'b', '1'], 'b1'); expect(object).toEqual({ a: { b: [undefined, 'b1'] } }); expect(object2).toEqual(object); - set(object, 'a.b.0', 5); + set(object, 'a.b[0]', 5); set(object2, ['a', 'b', 0], 5); expect(object).toEqual({ a: { b: [5, 'b1'] } }); expect(object2).toEqual(object); @@ -61,7 +65,7 @@ describe('Tests for set', () => { }); expect(object2).toEqual(object); - set(object, 'a.b.3.c', 'c'); + set(object, 'a.b[3].c', 'c'); set(object2, ['a', 'b', 3, 'c'], 'c'); expect(object).toEqual({ a: { b: [5, 'b1', 'b2', { c: 'c' }] }, @@ -69,7 +73,7 @@ describe('Tests for set', () => { expect(object2).toEqual(object); }); - it('creates an empty key', () => { + it('creates an empty string key', () => { expect(set({}, '', 'empty string')).toEqual({ '': 'empty string' }); expect(set({}, [''], 'empty string for array')).toEqual({ '': 'empty string for array', @@ -117,7 +121,7 @@ describe('Tests for set', () => { set(object, [2], '2'); expect(object).toEqual({ 1: 'test', 2: '2' }); - expect(set({ test: { test1: 'test' } }, 'test.1', 'test2')).toEqual({ + expect(set({ test: { test1: 'test' } }, 'test[1]', 'test2')).toEqual({ test: { '1': 'test2', test1: 'test' }, }); }); @@ -136,11 +140,17 @@ describe('Tests for set', () => { expect(obj4).toEqual({ 1: [undefined, [undefined, undefined, 'test']] }); }); - it('can uses symbols as keys', () => { - // do these count as symbols or strings here though... I was trying '\@', but eslint changed it to below + it('can use symbols as keys', () => { const object = {}; - expect(set(object, '%', 'test')).toEqual({ '%': 'test' }); - expect(set(object, ['@'], '2')).toEqual({ '%': 'test', '@': '2' }); + const symbolTest = Symbol('test'); + expect(set(object, symbolTest, 'test')).toEqual({ + [symbolTest]: 'test', + }); + const symbolAt = Symbol('@'); + expect(set(object, [symbolAt], '2')).toEqual({ + [symbolAt]: '2', + [symbolTest]: 'test', + }); }); it('sets the value at an already existing key', () => { diff --git a/packages/manager/src/utilities/set.ts b/packages/manager/src/utilities/set.ts index 62d2ec2785a..7572ca75b7a 100644 --- a/packages/manager/src/utilities/set.ts +++ b/packages/manager/src/utilities/set.ts @@ -35,10 +35,10 @@ export function set( } let updatingObject: any = object; - // if we haven't reached the last key, traverse through object, + // if we haven't reached the last key, traverse through object and for (let i = 0; i < updatedPath.length - 1; i++) { const key = updatedPath[i]; - // we set object[key] to be [] or {} depending on the next key + // set object[key] to be [] or {} depending on the next key if (!updatingObject[key] || typeof updatingObject[key] !== 'object') { const nextKey = updatedPath[i + 1]; updatingObject[key] = isValidIndex(nextKey) ? [] : {}; @@ -47,14 +47,20 @@ export function set( updatingObject = updatingObject[key]; } - // set value after reaching end of the path + // set value after reaching end of path updatingObject[updatedPath[updatedPath.length - 1]] = value; return object; } /** - * Helper to ensure a path cannot lead to a prototype pollution issue. + * ------------------------------------------------------------------------- + * Helper functions for set + * ------------------------------------------------------------------------- + */ + +/** + * Ensures a path cannot lead to a prototype pollution issue. * * @param path - The path to check * @return - If value is safe, returns it; otherwise returns undefined @@ -77,13 +83,17 @@ export const isPrototypePollutionSafe = (path: PropertyName[]): boolean => { * @returns - The path to set a value, in array format */ const determinePath = (path: PropertyPath): PropertyName[] => { - return Array.isArray(path) - ? path - : path.toString().match(/[^.[\]]+/g) ?? ['']; + if (Array.isArray(path)) { + return path; + } + + return typeof path === 'string' + ? path.toString().match(/[^.[\]]+/g) ?? [''] + : ([path] as PropertyName[]); }; /** - * Determines if the given value can be considered a valid index for an array. + * Determines if the given value can be considered a valid index for an array/string. * For example, 0, 1, 2 are all valid indexes. * -1, 01, -01, 00, '1 1' are not. * From 4e0ef19cd18260917fae5f275b776d970d2819eb Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Fri, 23 Aug 2024 13:47:36 -0400 Subject: [PATCH 14/17] changeset + update tests --- .../pr-10814-tech-stories-1724434847753.md | 5 ++ packages/manager/src/utilities/set.test.ts | 64 +++++++++++++++++-- packages/manager/src/utilities/set.ts | 42 ++++++------ 3 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 packages/manager/.changeset/pr-10814-tech-stories-1724434847753.md diff --git a/packages/manager/.changeset/pr-10814-tech-stories-1724434847753.md b/packages/manager/.changeset/pr-10814-tech-stories-1724434847753.md new file mode 100644 index 00000000000..cbe20d384c3 --- /dev/null +++ b/packages/manager/.changeset/pr-10814-tech-stories-1724434847753.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Replace lodash set utility function to handle security threat raised by Dependabot ([#10814](https://github.com/linode/manager/pull/10814)) diff --git a/packages/manager/src/utilities/set.test.ts b/packages/manager/src/utilities/set.test.ts index e6b6157e72b..683bd99c472 100644 --- a/packages/manager/src/utilities/set.test.ts +++ b/packages/manager/src/utilities/set.test.ts @@ -1,4 +1,9 @@ -import { isPrototypePollutionSafe, set } from './set'; +import { + determinePath, + isPrototypePollutionSafe, + isValidIndex, + set, +} from './set'; describe('Tests for set', () => { it("returns the passed in 'object' as is if it's not actually a (non array) object", () => { @@ -113,7 +118,7 @@ describe('Tests for set', () => { }); }); - it('considers numbers as keys if they are not used as indexes or if there is an already existing object', () => { + it('considers numbers as keys if they are not followed by another number or if there is an already existing object', () => { const object = {}; set(object, 1, 'test'); expect(object).toEqual({ 1: 'test' }); @@ -126,7 +131,7 @@ describe('Tests for set', () => { }); }); - it('treats later numbers as indexes (if they are valid indexes)', () => { + it('treats numbers as array indexes if they precede some previous key (if they are valid indexes)', () => { const obj1 = set({}, '1[1]', 'test'); expect(obj1).toEqual({ 1: [undefined, 'test'] }); @@ -153,7 +158,7 @@ describe('Tests for set', () => { }); }); - it('sets the value at an already existing key', () => { + it('can replace the value at an already existing key', () => { const alreadyExisting = { test: 'test' }; expect(set(alreadyExisting, 'test', 'changed')).toEqual({ test: 'changed', @@ -317,6 +322,30 @@ describe('Tests for set', () => { }); }); +describe('Tests for determinePath', () => { + const symbol = Symbol(); + it('returns array paths as is', () => { + expect(determinePath([])).toEqual([]); + expect(determinePath([symbol, '1', 1])).toEqual([symbol, '1', 1]); + }); + + it('determines string paths, parsing them as needed', () => { + expect(determinePath('')).toEqual(['']); + expect(determinePath('test')).toEqual(['test']); + expect(determinePath('test.test')).toEqual(['test', 'test']); + expect(determinePath('test[1]')).toEqual(['test', '1']); + expect(determinePath('a.b[1]')).toEqual(['a', 'b', '1']); + expect(determinePath('a.b[1].c')).toEqual(['a', 'b', '1', 'c']); + expect(determinePath('a.__proto__.c')).toEqual(['a', '__proto__', 'c']); + expect(determinePath('a.__proto__[2]')).toEqual(['a', '__proto__', '2']); + }); + + it('returns symbol and number paths inside an array', () => { + expect(determinePath(symbol)).toEqual([symbol]); + expect(determinePath(1)).toEqual([1]); + }); +}); + describe('Tests for isPrototypePollutionSafe', () => { it('determines that given array is prototype pollution safe', () => { expect(isPrototypePollutionSafe([])).toBe(true); @@ -333,3 +362,30 @@ describe('Tests for isPrototypePollutionSafe', () => { ); }); }); + +describe('Tests for isValidIndex', () => { + it('is a valid index', () => { + expect(isValidIndex(0)).toBe(true); + expect(isValidIndex(1)).toBe(true); + expect(isValidIndex('0')).toBe(true); + expect(isValidIndex('0')).toBe(true); + expect(isValidIndex(2432)).toBe(true); + expect(isValidIndex('2432')).toBe(true); + }); + + it('is not a valid index', () => { + expect(isValidIndex(Symbol(1))).toBe(false); + expect(isValidIndex(-1)).toBe(false); + expect(isValidIndex('00')).toBe(false); + expect(isValidIndex('abcd')).toBe(false); + expect(isValidIndex('-1')).toBe(false); + expect(isValidIndex('01')).toBe(false); + expect(isValidIndex('-01')).toBe(false); + expect(isValidIndex('00001')).toBe(false); + expect(isValidIndex('1 1')).toBe(false); + expect(isValidIndex('1 ')).toBe(false); + expect(isValidIndex(' 1 ')).toBe(false); + expect(isValidIndex(' 1')).toBe(false); + expect(isValidIndex('abcd')).toBe(false); + }); +}); diff --git a/packages/manager/src/utilities/set.ts b/packages/manager/src/utilities/set.ts index 7572ca75b7a..c52cb3a9768 100644 --- a/packages/manager/src/utilities/set.ts +++ b/packages/manager/src/utilities/set.ts @@ -26,7 +26,7 @@ export function set( const updatedPath = determinePath(path); if ( - // ensure that both the path and value will not lead to prototype pollution issues + // ensure that the path will not lead to prototype pollution issues // and that there is actually a path to set !isPrototypePollutionSafe(updatedPath) || updatedPath.length === 0 @@ -59,30 +59,14 @@ export function set( * ------------------------------------------------------------------------- */ -/** - * Ensures a path cannot lead to a prototype pollution issue. - * - * @param path - The path to check - * @return - If value is safe, returns it; otherwise returns undefined - */ -export const isPrototypePollutionSafe = (path: PropertyName[]): boolean => { - return path.reduce((safeSoFar, val) => { - const isCurKeySafe = - typeof val === 'string' - ? val !== '__proto__' && val !== 'prototype' && val !== 'constructor' - : true; - return safeSoFar && isCurKeySafe; - }, true); -}; - /** * Determines the path to set some value at, converting any string paths * into array format. * * @param path - The path to check - * @returns - The path to set a value, in array format + * @returns - The path to set a value at, in array format */ -const determinePath = (path: PropertyPath): PropertyName[] => { +export const determinePath = (path: PropertyPath): PropertyName[] => { if (Array.isArray(path)) { return path; } @@ -92,6 +76,22 @@ const determinePath = (path: PropertyPath): PropertyName[] => { : ([path] as PropertyName[]); }; +/** + * Ensures a path cannot lead to a prototype pollution issue. + * + * @param path - The path to check + * @return - boolean depending on whether the path is safe or not + */ +export const isPrototypePollutionSafe = (path: PropertyName[]): boolean => { + return path.reduce((safeSoFar, val) => { + const isCurKeySafe = + typeof val === 'string' + ? val !== '__proto__' && val !== 'prototype' && val !== 'constructor' + : true; + return safeSoFar && isCurKeySafe; + }, true); +}; + /** * Determines if the given value can be considered a valid index for an array/string. * For example, 0, 1, 2 are all valid indexes. @@ -99,9 +99,9 @@ const determinePath = (path: PropertyPath): PropertyName[] => { * * @param value - The value to check * - * @returns - Returns boolean + * @returns - Returns boolean depending on if value is a valid index or not */ -const isValidIndex = (value: PropertyName) => { +export const isValidIndex = (value: PropertyName) => { const convertedValue = value.toString(); return ( From 2ab0bf26a8b1072325af21d92bb29408c4672310 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 27 Aug 2024 10:19:42 -0400 Subject: [PATCH 15/17] simplify set, remove separate set utility files --- .../src/utilities/formikErrorUtils.test.ts | 193 +++++++++ .../manager/src/utilities/formikErrorUtils.ts | 58 ++- packages/manager/src/utilities/set.test.ts | 391 ------------------ packages/manager/src/utilities/set.ts | 111 ----- 4 files changed, 250 insertions(+), 503 deletions(-) delete mode 100644 packages/manager/src/utilities/set.test.ts delete mode 100644 packages/manager/src/utilities/set.ts diff --git a/packages/manager/src/utilities/formikErrorUtils.test.ts b/packages/manager/src/utilities/formikErrorUtils.test.ts index 245fd29361b..935df1ed92b 100644 --- a/packages/manager/src/utilities/formikErrorUtils.test.ts +++ b/packages/manager/src/utilities/formikErrorUtils.test.ts @@ -2,6 +2,7 @@ import { getFormikErrorsFromAPIErrors, handleAPIErrors, handleVPCAndSubnetErrors, + set, } from './formikErrorUtils'; const errorWithoutField = [{ reason: 'Internal server error' }]; @@ -201,3 +202,195 @@ describe('getFormikErrorsFromAPIErrors', () => { } }); }); + +describe('Tests for set', () => { + it("returns the passed in 'object' as is if it's not actually a (non array) object", () => { + expect(set([], 'path not needed', 3)).toEqual([]); + }); + + describe('Correctly setting the value at the given path', () => { + it('sets the value for a simple path', () => { + const object = {}; + let settedObject = set(object, 'test', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({ test: 1 }); + + settedObject = set(object, 'test2', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({ test: 1, test2: 1 }); + }); + + it('sets the value for complex string paths (without indexes)', () => { + const object = {}; + + set(object, 'a.b.c', 'c'); + expect(object).toEqual({ a: { b: { c: 'c' } } }); + + set(object, 'a.b.d', 'd'); + expect(object).toEqual({ + a: { b: { c: 'c', d: 'd' } }, + }); + + set(object, 'e[f][g]', 'g'); + expect(object).toEqual({ + a: { b: { c: 'c', d: 'd' } }, + e: { f: { g: 'g' } }, + }); + }); + + it('sets the value for complex string paths (with indexes)', () => { + const object = {}; + + set(object, 'a.b.1', 'b1'); + expect(object).toEqual({ a: { b: [undefined, 'b1'] } }); + set(object, 'a.b[0]', 5); + expect(object).toEqual({ a: { b: [5, 'b1'] } }); + + set(object, 'a.b.2', 'b2'); + expect(object).toEqual({ + a: { b: [5, 'b1', 'b2'] }, + }); + + set(object, 'a.b[3].c', 'c'); + expect(object).toEqual({ + a: { b: [5, 'b1', 'b2', { c: 'c' }] }, + }); + }); + + it('only considers 0 or positive integers for setting array values', () => { + const object = {}; + + expect(set(object, 'test[-01].test1', 'test')).toEqual({ + test: { '-01': { test1: 'test' } }, + }); + expect(set(object, 'test[-01][-02]', 'test2')).toEqual({ + test: { '-01': { '-02': 'test2', test1: 'test' } }, + }); + expect(set(object, 'test[ 02]', 'test3')).toEqual({ + test: { + ' 02': 'test3', + '-01': { '-02': 'test2', test1: 'test' }, + }, + }); + expect(set(object, 'test[0 0]', 'test4')).toEqual({ + test: { + ' 02': 'test3', + '-01': { '-02': 'test2', test1: 'test' }, + '0 0': 'test4', + }, + }); + }); + + it('considers numbers as keys if they are not followed by another number or if there is an already existing object', () => { + const object = {}; + set(object, '1', 'test'); + expect(object).toEqual({ 1: 'test' }); + + set(object, '2', '2'); + expect(object).toEqual({ 1: 'test', 2: '2' }); + + expect(set({ test: { test1: 'test' } }, 'test[1]', 'test2')).toEqual({ + test: { '1': 'test2', test1: 'test' }, + }); + }); + + it('treats numbers as array indexes if they precede some previous key (if they are valid indexes)', () => { + const obj1 = set({}, '1[1]', 'test'); + expect(obj1).toEqual({ 1: [undefined, 'test'] }); + + const obj2 = set({}, '1.2', 'test'); + expect(obj2).toEqual({ 1: [undefined, undefined, 'test'] }); + }); + + it('can replace the value at an already existing key', () => { + const alreadyExisting = { test: 'test' }; + expect(set(alreadyExisting, 'test', 'changed')).toEqual({ + test: 'changed', + }); + expect(set(alreadyExisting, 'test[test2][test3]', 'changed x4')).toEqual({ + test: { test2: { test3: 'changed x4' } }, + }); + }); + + it('sets the value for nonstandard paths', () => { + expect(set({}, 'test.[.test]', 'testing 2')).toEqual({ + test: { test: 'testing 2' }, + }); + expect(set({}, 'test.[te[st]', 'testing 3')).toEqual({ + test: { te: { st: 'testing 3' } }, + }); + expect(set({}, 'test.]test', 'testing 4')).toEqual({ + test: { test: 'testing 4' }, + }); + }); + }); + + describe('Ensuring safety against prototype pollution and that the passed in and returned object are the same', () => { + it('protects against the given string path matching a prototype pollution key', () => { + const object = {}; + // __proto__ + let settedObject = set(object, '__proto__', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + // constructor + settedObject = set(object, 'constructor', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + // prototype + settedObject = set(object, 'prototype', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + }); + + it('protects against the given string path containing prototype pollution keys that are separated by path delimiters', () => { + const object = {}; + // prototype pollution key separated by . + let settedObject = set(object, 'test.__proto__.test', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + settedObject = set(object, 'test.constructor.test', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + settedObject = set(object, 'test.prototype.test', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + // prototype pollution key separated by [] + settedObject = set(object, 'test.test[__proto__]', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + settedObject = set(object, 'test.test[constructor]', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + + settedObject = set(object, 'test.test[prototype]', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({}); + }); + + it('is not considered prototype pollution if the string paths have a key not separated by delimiters', () => { + const object = {}; + // prototype pollution key separated by . + let settedObject = set(object, 'test__proto__test', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({ test__proto__test: 1 }); + + settedObject = set(object, 'constructortest', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({ constructortest: 1, test__proto__test: 1 }); + + settedObject = set(object, 'testprototype', 1); + expect(object).toBe(settedObject); + expect(object).toEqual({ + constructortest: 1, + test__proto__test: 1, + testprototype: 1, + }); + }); + }); +}); diff --git a/packages/manager/src/utilities/formikErrorUtils.ts b/packages/manager/src/utilities/formikErrorUtils.ts index b17047942fe..8edafb2f89a 100644 --- a/packages/manager/src/utilities/formikErrorUtils.ts +++ b/packages/manager/src/utilities/formikErrorUtils.ts @@ -2,7 +2,6 @@ import { reverse } from 'ramda'; import { getAPIErrorOrDefault } from './errorUtils'; import { isNilOrEmpty } from './isNilOrEmpty'; -import { set } from './set'; import type { APIError } from '@linode/api-v4/lib/types'; import type { FormikErrors } from 'formik'; @@ -23,6 +22,63 @@ export const getFormikErrorsFromAPIErrors = ( }, {}); }; +// regex used in the below set function +const onlyDigitsRegex = /^\d+$/; + +/** + * Helper for getFormikErrorsFromAPIErrors, sets the given value at a specified path of the given object. + * Note that while we are using this function in place of lodash's set, it is not an exact replacement. + * This method both mutates the passed in object and returns it. + * + * @param object — The object to modify. + * @param path — The path of the property to set. + * @param value — The value to set. + * @return — Returns object. + */ +export const set = (obj: object, path: string, value: any): object => { + const parts = path.split(/\.|\[|\]/).filter(Boolean); + + // ensure that obj is not an array and that the path is prototype pollution safe + if (Array.isArray(obj) || !isPrototypePollutionSafe(parts)) { + return obj; + } + + parts.reduce((acc: any, part: string, index: number) => { + if (index === parts.length - 1) { + // Last part, set the value + acc[part] = value; + } else if (part.match(onlyDigitsRegex)) { + // Handle array indices + const arrayIndex = parseInt(part, 10); + acc[arrayIndex] = + acc[arrayIndex] ?? (parts[index + 1].match(onlyDigitsRegex) ? [] : {}); + } else { + // Handle nested objects + const potentialNextVal = parts[index + 1].match(onlyDigitsRegex) + ? [] + : {}; + acc[part] = typeof acc[part] === 'object' ? acc[part] : potentialNextVal; + } + return acc[part]; + }, obj); + + return obj; +}; + +/** + * Ensures a path cannot lead to a prototype pollution issue. + * + * @param path - The path to check + * @return - boolean depending on whether the path is safe or not + */ +const isPrototypePollutionSafe = (path: string[]): boolean => { + return path.reduce((safeSoFar, val) => { + const isCurKeySafe = + val !== '__proto__' && val !== 'prototype' && val !== 'constructor'; + return safeSoFar && isCurKeySafe; + }, true); +}; + export const handleFieldErrors = ( callback: (error: unknown) => void, fieldErrors: APIError[] = [] diff --git a/packages/manager/src/utilities/set.test.ts b/packages/manager/src/utilities/set.test.ts deleted file mode 100644 index 683bd99c472..00000000000 --- a/packages/manager/src/utilities/set.test.ts +++ /dev/null @@ -1,391 +0,0 @@ -import { - determinePath, - isPrototypePollutionSafe, - isValidIndex, - set, -} from './set'; - -describe('Tests for set', () => { - it("returns the passed in 'object' as is if it's not actually a (non array) object", () => { - expect(set([], 'path not needed', 3)).toEqual([]); - }); - - describe('Correctly setting the value at the given path', () => { - it('sets the value for a simple path for both string and array paths', () => { - const object = {}; - let settedObject = set(object, 'test', 1); - expect(object).toBe(settedObject); - expect(object).toEqual({ test: 1 }); - - settedObject = set(object, ['test2'], 1); - expect(object).toBe(settedObject); - expect(object).toEqual({ test: 1, test2: 1 }); - }); - - it('sets the value for complex string and array paths (without indexes)', () => { - const object = {}; - const object2 = {}; - - // the given paths are equivalent in string vs array format - set(object, 'a.b.c', 'c'); - set(object2, ['a', 'b', 'c'], 'c'); - expect(object).toEqual({ a: { b: { c: 'c' } } }); - expect(object2).toEqual(object); - - set(object, 'a.b.d', 'd'); - set(object2, ['a', 'b', 'd'], 'd'); - expect(object).toEqual({ - a: { b: { c: 'c', d: 'd' } }, - }); - expect(object2).toEqual(object); - - set(object, 'e[f][g]', 'g'); - set(object2, ['e', 'f', 'g'], 'g'); - expect(object).toEqual({ - a: { b: { c: 'c', d: 'd' } }, - e: { f: { g: 'g' } }, - }); - expect(object2).toEqual(object); - }); - - it('sets the value for complex string and array paths (with indexes)', () => { - const object = {}; - const object2 = {}; - - // the given paths are equivalent in string vs array format - set(object, 'a.b.1', 'b1'); - set(object2, ['a', 'b', '1'], 'b1'); - expect(object).toEqual({ a: { b: [undefined, 'b1'] } }); - expect(object2).toEqual(object); - set(object, 'a.b[0]', 5); - set(object2, ['a', 'b', 0], 5); - expect(object).toEqual({ a: { b: [5, 'b1'] } }); - expect(object2).toEqual(object); - - // If path is an array, indexes can be passed in as a string or as a number - set(object, 'a.b.2', 'b2'); - set(object2, ['a', 'b', 2], 'b2'); - expect(object).toEqual({ - a: { b: [5, 'b1', 'b2'] }, - }); - expect(object2).toEqual(object); - - set(object, 'a.b[3].c', 'c'); - set(object2, ['a', 'b', 3, 'c'], 'c'); - expect(object).toEqual({ - a: { b: [5, 'b1', 'b2', { c: 'c' }] }, - }); - expect(object2).toEqual(object); - }); - - it('creates an empty string key', () => { - expect(set({}, '', 'empty string')).toEqual({ '': 'empty string' }); - expect(set({}, [''], 'empty string for array')).toEqual({ - '': 'empty string for array', - }); - }); - - it('does not set anything if no path is given', () => { - expect(set({}, [], 'should not set')).toEqual({}); - }); - - it('only considers valid indexes for setting array values', () => { - const object = {}; - - expect(set(object, 'test[01].test1', 'test')).toEqual({ - test: { '01': { test1: 'test' } }, - }); - expect(set(object, 'test[01].[02]', 'test2')).toEqual({ - test: { '01': { '02': 'test2', test1: 'test' } }, - }); - expect(set(object, 'test[-01]', 'test3')).toEqual({ - test: { '-01': 'test3', '01': { '02': 'test2', test1: 'test' } }, - }); - expect(set(object, 'test[ 02]', 'test4')).toEqual({ - test: { - ' 02': 'test4', - '-01': 'test3', - '01': { '02': 'test2', test1: 'test' }, - }, - }); - expect(set(object, 'test[00]', 'test5')).toEqual({ - test: { - ' 02': 'test4', - '-01': 'test3', - '00': 'test5', - '01': { '02': 'test2', test1: 'test' }, - }, - }); - }); - - it('considers numbers as keys if they are not followed by another number or if there is an already existing object', () => { - const object = {}; - set(object, 1, 'test'); - expect(object).toEqual({ 1: 'test' }); - - set(object, [2], '2'); - expect(object).toEqual({ 1: 'test', 2: '2' }); - - expect(set({ test: { test1: 'test' } }, 'test[1]', 'test2')).toEqual({ - test: { '1': 'test2', test1: 'test' }, - }); - }); - - it('treats numbers as array indexes if they precede some previous key (if they are valid indexes)', () => { - const obj1 = set({}, '1[1]', 'test'); - expect(obj1).toEqual({ 1: [undefined, 'test'] }); - - const obj2 = set({}, '1.2', 'test'); - expect(obj2).toEqual({ 1: [undefined, undefined, 'test'] }); - - const obj3 = set({}, [3, 1], 'test'); - expect(obj3).toEqual({ 3: [undefined, 'test'] }); - - const obj4 = set({}, [1, 1, 2], 'test'); - expect(obj4).toEqual({ 1: [undefined, [undefined, undefined, 'test']] }); - }); - - it('can use symbols as keys', () => { - const object = {}; - const symbolTest = Symbol('test'); - expect(set(object, symbolTest, 'test')).toEqual({ - [symbolTest]: 'test', - }); - const symbolAt = Symbol('@'); - expect(set(object, [symbolAt], '2')).toEqual({ - [symbolAt]: '2', - [symbolTest]: 'test', - }); - }); - - it('can replace the value at an already existing key', () => { - const alreadyExisting = { test: 'test' }; - expect(set(alreadyExisting, 'test', 'changed')).toEqual({ - test: 'changed', - }); - expect(set(alreadyExisting, ['test'], 'changed again')).toEqual({ - test: 'changed again', - }); - expect(set(alreadyExisting, ['test', 'test2'], 'changed x3')).toEqual({ - test: { test2: 'changed x3' }, - }); - expect(set(alreadyExisting, 'test[test2][test3]', 'changed x4')).toEqual({ - test: { test2: { test3: 'changed x4' } }, - }); - }); - - it('sets the value for nonstandard paths', () => { - expect(set({}, 'test.[.test]', 'testing 2')).toEqual({ - test: { test: 'testing 2' }, - }); - expect(set({}, 'test.[te[st]', 'testing 3')).toEqual({ - test: { te: { st: 'testing 3' } }, - }); - expect(set({}, 'test.]test', 'testing 4')).toEqual({ - test: { test: 'testing 4' }, - }); - }); - - // it will take an incredible amount of effort to get set to this level of finesse - it.skip('sets the value for non standard paths - not working currently', () => { - expect(set({}, 'test.[]', 'testing 5')).toEqual({ - test: { '': { '': 'testing 5' } }, - }); - expect(set({}, '[].test', 'testing 6')).toEqual({ - '': { test: 'testing 6' }, - }); - expect(set({}, '.', 'testing 7')).toEqual({ - '': { '': 'testing 7' }, - }); - expect(set({}, '[', 'testing 8')).toEqual({ - '[': 'testing 8', - }); - expect(set({}, ']', 'testing 9')).toEqual({ - ']': 'testing 9', - }); - expect(set({}, 'test..test', 'testing')).toEqual({ - test: { '': { test: 'testing' } }, - }); - }); - }); - - describe('Ensuring safety against prototype pollution and that the passed in and returned object are the same', () => { - it('protects against the given string path matching a prototype pollution key', () => { - const object = {}; - // __proto__ - let settedObject = set(object, '__proto__', 1); - expect(object).toBe(settedObject); - expect(object).toEqual({}); - - // constructor - settedObject = set(object, 'constructor', 1); - expect(object).toBe(settedObject); - expect(object).toEqual({}); - - // prototype - settedObject = set(object, 'prototype', 1); - expect(object).toBe(settedObject); - expect(object).toEqual({}); - }); - - it('protects against the given string path containing prototype pollution keys that are separated by path delimiters', () => { - const object = {}; - // prototype pollution key separated by . - let settedObject = set(object, 'test.__proto__.test', 1); - expect(object).toBe(settedObject); - expect(object).toEqual({}); - - settedObject = set(object, 'test.constructor.test', 1); - expect(object).toBe(settedObject); - expect(object).toEqual({}); - - settedObject = set(object, 'test.prototype.test', 1); - expect(object).toBe(settedObject); - expect(object).toEqual({}); - - // prototype pollution key separated by [] - settedObject = set(object, 'test.test[__proto__]', 1); - expect(object).toBe(settedObject); - expect(object).toEqual({}); - - settedObject = set(object, 'test.test[constructor]', 1); - expect(object).toBe(settedObject); - expect(object).toEqual({}); - - settedObject = set(object, 'test.test[prototype]', 1); - expect(object).toBe(settedObject); - expect(object).toEqual({}); - }); - - it('checks for prototype pollution keys in array paths', () => { - const object = {}; - let settedObject = set(object, [1, 2, '__proto__'], 1); - expect(object).toBe(settedObject); - expect(object).toEqual({}); - - settedObject = set(object, ['constructor', 'test', 'test'], 1); - expect(object).toBe(settedObject); - expect(object).toEqual({}); - - settedObject = set(object, ['test', 'prototype', 'test', 3], 1); - expect(object).toBe(settedObject); - expect(object).toEqual({}); - }); - - it('is not considered prototype pollution if the string paths have a key not separated by delimiters', () => { - const object = {}; - // prototype pollution key separated by . - let settedObject = set(object, 'test__proto__test', 1); - expect(object).toBe(settedObject); - expect(object).toEqual({ test__proto__test: 1 }); - - settedObject = set(object, 'constructortest', 1); - expect(object).toBe(settedObject); - expect(object).toEqual({ constructortest: 1, test__proto__test: 1 }); - - settedObject = set(object, 'testprototype', 1); - expect(object).toBe(settedObject); - expect(object).toEqual({ - constructortest: 1, - test__proto__test: 1, - testprototype: 1, - }); - }); - - it('is not considered prototype pollution if array paths do not have standalone prototype pollution keys', () => { - const object = {}; - let settedObject = set(object, ['test__proto__test'], 1); - expect(object).toBe(settedObject); - expect(object).toEqual({ test__proto__test: 1 }); - - settedObject = set(object, ['__proto__.test'], 1); - expect(object).toBe(settedObject); - expect(object).toEqual({ '__proto__.test': 1, test__proto__test: 1 }); - - settedObject = set(object, ['constructortest', 'test'], 1); - expect(object).toBe(settedObject); - expect(object).toEqual({ - '__proto__.test': 1, - constructortest: { test: 1 }, - test__proto__test: 1, - }); - - settedObject = set(object, ['testprototype'], 1); - expect(object).toBe(settedObject); - expect(object).toEqual({ - '__proto__.test': 1, - constructortest: { test: 1 }, - test__proto__test: 1, - testprototype: 1, - }); - }); - }); -}); - -describe('Tests for determinePath', () => { - const symbol = Symbol(); - it('returns array paths as is', () => { - expect(determinePath([])).toEqual([]); - expect(determinePath([symbol, '1', 1])).toEqual([symbol, '1', 1]); - }); - - it('determines string paths, parsing them as needed', () => { - expect(determinePath('')).toEqual(['']); - expect(determinePath('test')).toEqual(['test']); - expect(determinePath('test.test')).toEqual(['test', 'test']); - expect(determinePath('test[1]')).toEqual(['test', '1']); - expect(determinePath('a.b[1]')).toEqual(['a', 'b', '1']); - expect(determinePath('a.b[1].c')).toEqual(['a', 'b', '1', 'c']); - expect(determinePath('a.__proto__.c')).toEqual(['a', '__proto__', 'c']); - expect(determinePath('a.__proto__[2]')).toEqual(['a', '__proto__', '2']); - }); - - it('returns symbol and number paths inside an array', () => { - expect(determinePath(symbol)).toEqual([symbol]); - expect(determinePath(1)).toEqual([1]); - }); -}); - -describe('Tests for isPrototypePollutionSafe', () => { - it('determines that given array is prototype pollution safe', () => { - expect(isPrototypePollutionSafe([])).toBe(true); - expect(isPrototypePollutionSafe(['', 'test'])).toBe(true); - expect(isPrototypePollutionSafe(['', 1])).toBe(true); - }); - - it('determines that the given array is not prototype pollution safe', () => { - expect(isPrototypePollutionSafe(['__proto__'])).toBe(false); - expect(isPrototypePollutionSafe(['', 'test', 'prototype'])).toBe(false); - expect(isPrototypePollutionSafe(['', 1, 'constructor'])).toBe(false); - expect(isPrototypePollutionSafe(['', 1, 'constructor', '__proto__'])).toBe( - false - ); - }); -}); - -describe('Tests for isValidIndex', () => { - it('is a valid index', () => { - expect(isValidIndex(0)).toBe(true); - expect(isValidIndex(1)).toBe(true); - expect(isValidIndex('0')).toBe(true); - expect(isValidIndex('0')).toBe(true); - expect(isValidIndex(2432)).toBe(true); - expect(isValidIndex('2432')).toBe(true); - }); - - it('is not a valid index', () => { - expect(isValidIndex(Symbol(1))).toBe(false); - expect(isValidIndex(-1)).toBe(false); - expect(isValidIndex('00')).toBe(false); - expect(isValidIndex('abcd')).toBe(false); - expect(isValidIndex('-1')).toBe(false); - expect(isValidIndex('01')).toBe(false); - expect(isValidIndex('-01')).toBe(false); - expect(isValidIndex('00001')).toBe(false); - expect(isValidIndex('1 1')).toBe(false); - expect(isValidIndex('1 ')).toBe(false); - expect(isValidIndex(' 1 ')).toBe(false); - expect(isValidIndex(' 1')).toBe(false); - expect(isValidIndex('abcd')).toBe(false); - }); -}); diff --git a/packages/manager/src/utilities/set.ts b/packages/manager/src/utilities/set.ts deleted file mode 100644 index c52cb3a9768..00000000000 --- a/packages/manager/src/utilities/set.ts +++ /dev/null @@ -1,111 +0,0 @@ -type PropertyName = number | string | symbol; -type PropertyPath = PropertyName | readonly PropertyName[]; - -/** - * Helper to set the given value at the given path of the given object. - * This util serves as a replacement for `set` from lodash, with additional checks - * to protect against prototype pollution. It is not (yet) a perfect replacement (see - * skipped test examples in set.test.ts), but does handle current CM use cases. - * - * @param object — The object to modify. - * @param path — The path of the property to set. - * @param value — The value to set. - * @return — Returns object. - */ -export function set( - object: T, - path: PropertyPath, - value: any -): T { - // ensure that object is actually a non-array object - if (!object || Array.isArray(object) || typeof object !== 'object') { - return object; - } - - // if the path is not an array, convert it to an array format - const updatedPath = determinePath(path); - - if ( - // ensure that the path will not lead to prototype pollution issues - // and that there is actually a path to set - !isPrototypePollutionSafe(updatedPath) || - updatedPath.length === 0 - ) { - return object; - } - - let updatingObject: any = object; - // if we haven't reached the last key, traverse through object and - for (let i = 0; i < updatedPath.length - 1; i++) { - const key = updatedPath[i]; - // set object[key] to be [] or {} depending on the next key - if (!updatingObject[key] || typeof updatingObject[key] !== 'object') { - const nextKey = updatedPath[i + 1]; - updatingObject[key] = isValidIndex(nextKey) ? [] : {}; - } - - updatingObject = updatingObject[key]; - } - - // set value after reaching end of path - updatingObject[updatedPath[updatedPath.length - 1]] = value; - - return object; -} - -/** - * ------------------------------------------------------------------------- - * Helper functions for set - * ------------------------------------------------------------------------- - */ - -/** - * Determines the path to set some value at, converting any string paths - * into array format. - * - * @param path - The path to check - * @returns - The path to set a value at, in array format - */ -export const determinePath = (path: PropertyPath): PropertyName[] => { - if (Array.isArray(path)) { - return path; - } - - return typeof path === 'string' - ? path.toString().match(/[^.[\]]+/g) ?? [''] - : ([path] as PropertyName[]); -}; - -/** - * Ensures a path cannot lead to a prototype pollution issue. - * - * @param path - The path to check - * @return - boolean depending on whether the path is safe or not - */ -export const isPrototypePollutionSafe = (path: PropertyName[]): boolean => { - return path.reduce((safeSoFar, val) => { - const isCurKeySafe = - typeof val === 'string' - ? val !== '__proto__' && val !== 'prototype' && val !== 'constructor' - : true; - return safeSoFar && isCurKeySafe; - }, true); -}; - -/** - * Determines if the given value can be considered a valid index for an array/string. - * For example, 0, 1, 2 are all valid indexes. - * -1, 01, -01, 00, '1 1' are not. - * - * @param value - The value to check - * - * @returns - Returns boolean depending on if value is a valid index or not - */ -export const isValidIndex = (value: PropertyName) => { - const convertedValue = value.toString(); - - return ( - /^\d+$/.test(convertedValue) && // must be all digits - (convertedValue.length < 2 || !convertedValue.startsWith('0')) // must not start with 0 (unless it is 0 only) - ); -}; From 01c6e76e7273e1b9ebeff2c1a5861e59718d7106 Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:24:09 -0400 Subject: [PATCH 16/17] Update packages/manager/src/utilities/formikErrorUtils.ts Co-authored-by: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> --- packages/manager/src/utilities/formikErrorUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/utilities/formikErrorUtils.ts b/packages/manager/src/utilities/formikErrorUtils.ts index 8edafb2f89a..8136ab05493 100644 --- a/packages/manager/src/utilities/formikErrorUtils.ts +++ b/packages/manager/src/utilities/formikErrorUtils.ts @@ -43,7 +43,7 @@ export const set = (obj: object, path: string, value: any): object => { return obj; } - parts.reduce((acc: any, part: string, index: number) => { + parts.reduce((acc: Record, part: string, index: number) => { if (index === parts.length - 1) { // Last part, set the value acc[part] = value; From b4c000e87f68bcca05fbd7620dee4cfae7c50436 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 27 Aug 2024 11:36:34 -0400 Subject: [PATCH 17/17] update types + fix tests @abailly-akamai --- .../src/utilities/formikErrorUtils.test.ts | 60 +++++++++---------- .../manager/src/utilities/formikErrorUtils.ts | 6 +- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/manager/src/utilities/formikErrorUtils.test.ts b/packages/manager/src/utilities/formikErrorUtils.test.ts index 935df1ed92b..48adf78a959 100644 --- a/packages/manager/src/utilities/formikErrorUtils.test.ts +++ b/packages/manager/src/utilities/formikErrorUtils.test.ts @@ -205,19 +205,19 @@ describe('getFormikErrorsFromAPIErrors', () => { describe('Tests for set', () => { it("returns the passed in 'object' as is if it's not actually a (non array) object", () => { - expect(set([], 'path not needed', 3)).toEqual([]); + expect(set([], 'path not needed', '1')).toEqual([]); }); describe('Correctly setting the value at the given path', () => { it('sets the value for a simple path', () => { const object = {}; - let settedObject = set(object, 'test', 1); + let settedObject = set(object, 'test', '1'); expect(object).toBe(settedObject); - expect(object).toEqual({ test: 1 }); + expect(object).toEqual({ test: '1' }); - settedObject = set(object, 'test2', 1); + settedObject = set(object, 'test2', '1'); expect(object).toBe(settedObject); - expect(object).toEqual({ test: 1, test2: 1 }); + expect(object).toEqual({ test: '1', test2: '1' }); }); it('sets the value for complex string paths (without indexes)', () => { @@ -243,17 +243,17 @@ describe('Tests for set', () => { set(object, 'a.b.1', 'b1'); expect(object).toEqual({ a: { b: [undefined, 'b1'] } }); - set(object, 'a.b[0]', 5); - expect(object).toEqual({ a: { b: [5, 'b1'] } }); + set(object, 'a.b[0]', '5'); + expect(object).toEqual({ a: { b: ['5', 'b1'] } }); set(object, 'a.b.2', 'b2'); expect(object).toEqual({ - a: { b: [5, 'b1', 'b2'] }, + a: { b: ['5', 'b1', 'b2'] }, }); set(object, 'a.b[3].c', 'c'); expect(object).toEqual({ - a: { b: [5, 'b1', 'b2', { c: 'c' }] }, + a: { b: ['5', 'b1', 'b2', { c: 'c' }] }, }); }); @@ -281,20 +281,16 @@ describe('Tests for set', () => { }); }); - it('considers numbers as keys if they are not followed by another number or if there is an already existing object', () => { + it('considers numbers as keys if they are not followed by another number', () => { const object = {}; set(object, '1', 'test'); expect(object).toEqual({ 1: 'test' }); set(object, '2', '2'); expect(object).toEqual({ 1: 'test', 2: '2' }); - - expect(set({ test: { test1: 'test' } }, 'test[1]', 'test2')).toEqual({ - test: { '1': 'test2', test1: 'test' }, - }); }); - it('treats numbers as array indexes if they precede some previous key (if they are valid indexes)', () => { + it('treats numbers as array indexes if they precede some previous key (if they are valid integers >= 0)', () => { const obj1 = set({}, '1[1]', 'test'); expect(obj1).toEqual({ 1: [undefined, 'test'] }); @@ -329,17 +325,17 @@ describe('Tests for set', () => { it('protects against the given string path matching a prototype pollution key', () => { const object = {}; // __proto__ - let settedObject = set(object, '__proto__', 1); + let settedObject = set(object, '__proto__', '1'); expect(object).toBe(settedObject); expect(object).toEqual({}); // constructor - settedObject = set(object, 'constructor', 1); + settedObject = set(object, 'constructor', '1'); expect(object).toBe(settedObject); expect(object).toEqual({}); // prototype - settedObject = set(object, 'prototype', 1); + settedObject = set(object, 'prototype', '1'); expect(object).toBe(settedObject); expect(object).toEqual({}); }); @@ -347,28 +343,28 @@ describe('Tests for set', () => { it('protects against the given string path containing prototype pollution keys that are separated by path delimiters', () => { const object = {}; // prototype pollution key separated by . - let settedObject = set(object, 'test.__proto__.test', 1); + let settedObject = set(object, 'test.__proto__.test', '1'); expect(object).toBe(settedObject); expect(object).toEqual({}); - settedObject = set(object, 'test.constructor.test', 1); + settedObject = set(object, 'test.constructor.test', '1'); expect(object).toBe(settedObject); expect(object).toEqual({}); - settedObject = set(object, 'test.prototype.test', 1); + settedObject = set(object, 'test.prototype.test', '1'); expect(object).toBe(settedObject); expect(object).toEqual({}); // prototype pollution key separated by [] - settedObject = set(object, 'test.test[__proto__]', 1); + settedObject = set(object, 'test.test[__proto__]', '1'); expect(object).toBe(settedObject); expect(object).toEqual({}); - settedObject = set(object, 'test.test[constructor]', 1); + settedObject = set(object, 'test.test[constructor]', '1'); expect(object).toBe(settedObject); expect(object).toEqual({}); - settedObject = set(object, 'test.test[prototype]', 1); + settedObject = set(object, 'test.test[prototype]', '1'); expect(object).toBe(settedObject); expect(object).toEqual({}); }); @@ -376,20 +372,20 @@ describe('Tests for set', () => { it('is not considered prototype pollution if the string paths have a key not separated by delimiters', () => { const object = {}; // prototype pollution key separated by . - let settedObject = set(object, 'test__proto__test', 1); + let settedObject = set(object, 'test__proto__test', '1'); expect(object).toBe(settedObject); - expect(object).toEqual({ test__proto__test: 1 }); + expect(object).toEqual({ test__proto__test: '1' }); - settedObject = set(object, 'constructortest', 1); + settedObject = set(object, 'constructortest', '1'); expect(object).toBe(settedObject); - expect(object).toEqual({ constructortest: 1, test__proto__test: 1 }); + expect(object).toEqual({ constructortest: '1', test__proto__test: '1' }); - settedObject = set(object, 'testprototype', 1); + settedObject = set(object, 'testprototype', '1'); expect(object).toBe(settedObject); expect(object).toEqual({ - constructortest: 1, - test__proto__test: 1, - testprototype: 1, + constructortest: '1', + test__proto__test: '1', + testprototype: '1', }); }); }); diff --git a/packages/manager/src/utilities/formikErrorUtils.ts b/packages/manager/src/utilities/formikErrorUtils.ts index 8136ab05493..1f46a13d6ea 100644 --- a/packages/manager/src/utilities/formikErrorUtils.ts +++ b/packages/manager/src/utilities/formikErrorUtils.ts @@ -35,7 +35,11 @@ const onlyDigitsRegex = /^\d+$/; * @param value — The value to set. * @return — Returns object. */ -export const set = (obj: object, path: string, value: any): object => { +export const set = ( + obj: FormikErrors, + path: string, + value: string +): FormikErrors => { const parts = path.split(/\.|\[|\]/).filter(Boolean); // ensure that obj is not an array and that the path is prototype pollution safe