Remove dependency on lodash.pick. (#15213)

Fixes [Dependabot Alert
85](https://github.com/twentyhq/twenty/security/dependabot/85) -
prototype pollution in lodash.

Added a shared pick helper (with unit tests) in twenty-shared and
refactored front-end/server code to import { pick } from the shared
barrel instead of lodash.pick.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: martmull <martmull@hotmail.fr>
This commit is contained in:
Abdullah.
2025-10-21 13:19:49 +05:00
committed by GitHub
parent 187cf400aa
commit 793dc3d6fc
14 changed files with 67 additions and 90 deletions
-2
View File
@@ -42,7 +42,6 @@
"lodash.mapvalues": "^4.6.0",
"lodash.merge": "^4.6.2",
"lodash.omit": "^4.5.0",
"lodash.pick": "^4.4.0",
"lodash.pickby": "^4.6.0",
"lodash.snakecase": "^4.1.1",
"lodash.upperfirst": "^4.3.1",
@@ -137,7 +136,6 @@
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.mapvalues": "^4.6.9",
"@types/lodash.omit": "^4.5.9",
"@types/lodash.pick": "^4.3.7",
"@types/lodash.pickby": "^4.6.9",
"@types/lodash.snakecase": "^4.1.7",
"@types/lodash.upperfirst": "^4.3.7",
@@ -1,7 +1,6 @@
import { type MockedResponse } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import gql from 'graphql-tag';
import pick from 'lodash.pick';
import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@@ -12,8 +11,13 @@ const mockedDate = '2024-03-15T12:00:00.000Z';
const toISOStringMock = jest.fn(() => mockedDate);
global.Date.prototype.toISOString = toISOStringMock;
const { id, title, bodyV2, status, dueAt } = mockedTasks[0];
const mockedActivity = {
...pick(mockedTasks[0], ['id', 'title', 'bodyV2', 'type', 'status', 'dueAt']),
id,
title,
bodyV2,
status,
dueAt,
updatedAt: mockedDate,
};
@@ -7,7 +7,6 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import gql from 'graphql-tag';
import pick from 'lodash.pick';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { mockedTasks } from '~/testing/mock-data/tasks';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
@@ -16,8 +15,13 @@ const mockedDate = '2024-03-15T12:00:00.000Z';
const toISOStringMock = jest.fn(() => mockedDate);
global.Date.prototype.toISOString = toISOStringMock;
const { id, title, bodyV2, status, dueAt } = mockedTasks[0];
const mockedActivity = {
...pick(mockedTasks[0], ['id', 'title', 'bodyV2', 'type', 'status', 'dueAt']),
id,
title,
bodyV2,
status,
dueAt,
updatedAt: mockedDate,
};
@@ -1,16 +1,16 @@
import pick from 'lodash.pick';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { isDefined } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const getRecordFromRecordNode = <T extends ObjectRecord>({
recordNode,
}: {
recordNode: RecordGqlNode;
}): T => {
const { id, __typename } = recordNode;
return {
...Object.fromEntries(
Object.entries(recordNode).map(([fieldName, value]) => {
@@ -34,6 +34,7 @@ export const getRecordFromRecordNode = <T extends ObjectRecord>({
// RawJson field value passes through this method and does not have `id` or `__typename`.
// This prevents adding an undefined `id` and `__typename` to the RawJson field value,
// which is invalid JSON.
...pick(recordNode, ['id', '__typename'] as const),
...(isDefined(id) ? { id } : {}),
...(isDefined(__typename) ? { __typename } : {}),
} as T;
};
@@ -14,7 +14,6 @@ import { ApolloError } from '@apollo/client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Section } from '@react-email/components';
import pick from 'lodash.pick';
import { FormProvider, useForm } from 'react-hook-form';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
@@ -75,11 +74,17 @@ export const SettingsIntegrationEditDatabaseConnectionContent = ({
formConfig.formState.dirtyFields,
) as (keyof SettingsIntegrationEditConnectionFormValues)[];
const dirtyFormValues = Object.fromEntries(
Object.entries(formValues).filter(([key]) =>
dirtyFieldKeys.includes(key as keyof typeof formValues),
),
);
try {
await updateOneDatabaseConnection({
...formatValuesForUpdate({
databaseKey,
formValues: pick(formValues, dirtyFieldKeys),
formValues: dirtyFormValues,
}),
id: connection?.id ?? '',
});
@@ -1,6 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod';
import omit from 'lodash.omit';
import pick from 'lodash.pick';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
@@ -13,6 +12,7 @@ import { useUpdateOneFieldMetadataItem } from '@/object-metadata/hooks/useUpdate
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { formatFieldMetadataItemInput } from '@/object-metadata/utils/formatFieldMetadataItemInput';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/FieldNameMaximumLength';
@@ -36,7 +36,6 @@ import { Section } from 'twenty-ui/layout';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
//TODO: fix this type
export type SettingsDataModelFieldEditFormValues = z.infer<
@@ -151,9 +150,10 @@ export const SettingsObjectFieldEdit = () => {
const otherDirtyFields = omit(dirtyFields, 'relation');
if (Object.keys(otherDirtyFields).length > 0) {
const formattedInput = pick(
formatFieldMetadataItemInput(formValues),
Object.keys(otherDirtyFields),
const formattedInput = Object.fromEntries(
Object.entries(formatFieldMetadataItemInput(formValues)).filter(
([key]) => Object.keys(otherDirtyFields).includes(key),
),
);
await updateOneFieldMetadataItem({
@@ -12,7 +12,6 @@ import { ApolloError } from '@apollo/client';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import pick from 'lodash.pick';
import { FormProvider, useForm } from 'react-hook-form';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
@@ -37,13 +36,17 @@ export const SettingsSecuritySSOIdentifyProvider = () => {
try {
const type = form.getValues('type');
const values = form.getValues();
const providerKeys = Object.keys(
sSOIdentityProviderDefaultValues[type](),
);
const filteredValues = Object.fromEntries(
Object.entries(values).filter(([key]) => providerKeys.includes(key)),
);
await createSSOIdentityProvider(
SSOIdentitiesProvidersParamsSchema.parse(
pick(
form.getValues(),
Object.keys(sSOIdentityProviderDefaultValues[type]()),
),
),
SSOIdentitiesProvidersParamsSchema.parse(filteredValues),
);
navigate(SettingsPath.Security);
-1
View File
@@ -135,7 +135,6 @@
"lodash.merge": "^4.6.2",
"lodash.omit": "4.5.0",
"lodash.omitby": "^4.6.0",
"lodash.pick": "4.4.0",
"lodash.snakecase": "4.1.1",
"lodash.uniq": "^4.5.0",
"lodash.uniqby": "^4.7.0",
@@ -1,11 +1,11 @@
import { Injectable } from '@nestjs/common';
import { type Request } from 'express';
import pick from 'lodash.pick';
import { isDefined } from 'twenty-shared/utils';
import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory';
import { MCPMetadataToolsService } from 'src/engine/api/mcp/services/tools/mcp-metadata-tools.service';
import { validationSchemaManager } from 'src/engine/api/mcp/utils/get-json-schema';
import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory';
import { type ObjectName } from 'src/engine/api/rest/metadata/types/metadata-entity.type';
@Injectable()
@@ -39,18 +39,22 @@ export class CreateToolsService {
}
async execute(request: Request, objectName: ObjectName) {
const { fields, objects } = request.body.params.arguments;
const selectors = {
...(isDefined(fields) ? { fields } : {}),
...(isDefined(objects) ? { objects } : {}),
};
const requestContext = {
body: request.body.params.arguments,
baseUrl: this.mCPMetadataToolsService.generateBaseUrl(request),
path: `/rest/metadata/${objectName}`,
headers: request.headers,
};
const response = await this.mCPMetadataToolsService.send(
requestContext,
await this.metadataQueryBuilderFactory.create(
requestContext,
pick(request.body.params.arguments, ['fields', 'objects']),
),
await this.metadataQueryBuilderFactory.create(requestContext, selectors),
);
return response.data.data;
@@ -1,10 +1,10 @@
import { Injectable } from '@nestjs/common';
import { type Request } from 'express';
import pick from 'lodash.pick';
import { isDefined } from 'twenty-shared/utils';
import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory';
import { MCPMetadataToolsService } from 'src/engine/api/mcp/services/tools/mcp-metadata-tools.service';
import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory';
import { type ObjectName } from 'src/engine/api/rest/metadata/types/metadata-entity.type';
@Injectable()
@@ -74,6 +74,12 @@ export class GetToolsService {
}
async execute(request: Request, objectName: ObjectName) {
const { fields, objects } = request.body.params.arguments;
const selectors = {
...(isDefined(fields) ? { fields } : {}),
...(isDefined(objects) ? { objects } : {}),
};
const requestContext = {
body: request.body.params.arguments,
baseUrl: this.mCPMetadataToolsService.generateBaseUrl(request),
@@ -84,10 +90,7 @@ export class GetToolsService {
const response = await this.mCPMetadataToolsService.send(
requestContext,
await this.metadataQueryBuilderFactory.get(
requestContext,
pick(request.body.params.arguments, ['fields', 'objects']),
),
await this.metadataQueryBuilderFactory.get(requestContext, selectors),
);
return response.data.data;
@@ -2,11 +2,11 @@ import { Injectable } from '@nestjs/common';
import { type Request } from 'express';
import omit from 'lodash.omit';
import pick from 'lodash.pick';
import { isDefined } from 'twenty-shared/utils';
import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory';
import { MCPMetadataToolsService } from 'src/engine/api/mcp/services/tools/mcp-metadata-tools.service';
import { validationSchemaManager } from 'src/engine/api/mcp/utils/get-json-schema';
import { MetadataQueryBuilderFactory } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.factory';
import { type ObjectName } from 'src/engine/api/rest/metadata/types/metadata-entity.type';
@Injectable()
@@ -70,7 +70,12 @@ export class UpdateToolsService {
}
async execute(request: Request, objectName: ObjectName) {
const { id, ...body } = request.body.params.arguments;
const { id, fields, objects, ...body } = request.body.params.arguments;
const selectors = {
...(isDefined(fields) ? { fields } : {}),
...(isDefined(objects) ? { objects } : {}),
};
const requestContext = {
body,
baseUrl: this.mCPMetadataToolsService.generateBaseUrl(request),
@@ -80,10 +85,7 @@ export class UpdateToolsService {
const response = await this.mCPMetadataToolsService.send(
requestContext,
await this.metadataQueryBuilderFactory.update(
requestContext,
pick(request.body.params.arguments, ['fields', 'objects']),
),
await this.metadataQueryBuilderFactory.update(requestContext, selectors),
);
return response.data.data;
@@ -34,7 +34,6 @@
"lodash.mapvalues": "^4.6.0",
"lodash.merge": "^4.6.2",
"lodash.omit": "^4.5.0",
"lodash.pick": "^4.4.0",
"lodash.pickby": "^4.6.0",
"lodash.snakecase": "^4.1.1",
"lodash.upperfirst": "^4.3.1",
@@ -303,15 +303,6 @@ __metadata:
languageName: node
linkType: hard
"@types/lodash.debounce@npm:^4.0.9":
version: 4.0.9
resolution: "@types/lodash.debounce@npm:4.0.9"
dependencies:
"@types/lodash": "npm:*"
checksum: 10c0/9fbb24e5e52616faf60ba5c82d8c6517f4b86fc6e9ab353b4c56c0760f63d9bf53af3f2d8f6c37efa48090359fb96dba1087d497758511f6c40677002191d042
languageName: node
linkType: hard
"@types/lodash.groupby@npm:^4.6.9":
version: 4.6.9
resolution: "@types/lodash.groupby@npm:4.6.9"
@@ -1796,13 +1787,6 @@ __metadata:
languageName: node
linkType: hard
"lodash.debounce@npm:^4.0.8":
version: 4.0.8
resolution: "lodash.debounce@npm:4.0.8"
checksum: 10c0/762998a63e095412b6099b8290903e0a8ddcb353ac6e2e0f2d7e7d03abd4275fe3c689d88960eb90b0dde4f177554d51a690f22a343932ecbc50a5d111849987
languageName: node
linkType: hard
"lodash.groupby@npm:^4.6.0":
version: 4.6.0
resolution: "lodash.groupby@npm:4.6.0"
@@ -1915,13 +1899,6 @@ __metadata:
languageName: node
linkType: hard
"lodash.pick@npm:^4.4.0":
version: 4.4.0
resolution: "lodash.pick@npm:4.4.0"
checksum: 10c0/a04c460b95d1aaa44e9513d1dacf72ea74d838da843e45831de9de64c303f13cdde1859702a6f4dcef417816898ffd47c6ae0614c957ac70245bed2809b8d2e2
languageName: node
linkType: hard
"lodash.pickby@npm:^4.6.0":
version: 4.6.0
resolution: "lodash.pickby@npm:4.6.0"
@@ -2531,7 +2508,6 @@ __metadata:
"@types/deep-equal": "npm:^1.0.4"
"@types/lodash.camelcase": "npm:^4.3.9"
"@types/lodash.compact": "npm:^3.0.9"
"@types/lodash.debounce": "npm:^4.0.9"
"@types/lodash.groupby": "npm:^4.6.9"
"@types/lodash.identity": "npm:^3.0.9"
"@types/lodash.isempty": "npm:^4.4.9"
@@ -2553,7 +2529,6 @@ __metadata:
lodash.camelcase: "npm:^4.3.0"
lodash.chunk: "npm:^4.2.0"
lodash.compact: "npm:^3.0.1"
lodash.debounce: "npm:^4.0.8"
lodash.groupby: "npm:^4.6.0"
lodash.identity: "npm:^3.0.0"
lodash.isempty: "npm:^4.4.0"
@@ -2563,7 +2538,6 @@ __metadata:
lodash.mapvalues: "npm:^4.6.0"
lodash.merge: "npm:^4.6.2"
lodash.omit: "npm:^4.5.0"
lodash.pick: "npm:^4.4.0"
lodash.pickby: "npm:^4.6.0"
lodash.snakecase: "npm:^4.1.1"
lodash.upperfirst: "npm:^4.3.1"
-19
View File
@@ -21627,15 +21627,6 @@ __metadata:
languageName: node
linkType: hard
"@types/lodash.pick@npm:^4.3.7":
version: 4.4.9
resolution: "@types/lodash.pick@npm:4.4.9"
dependencies:
"@types/lodash": "npm:*"
checksum: 10c0/61ae2fb9fe817a2f398863d10dc57e3cb864ebadefb7f3554d8d0c189448e08a174d6bfdd6e0220988ea084a2d47c3377908cf037e2b3ce552aa79ce0f36bc9f
languageName: node
linkType: hard
"@types/lodash.pickby@npm:^4.6.9":
version: 4.6.9
resolution: "@types/lodash.pickby@npm:4.6.9"
@@ -39620,13 +39611,6 @@ __metadata:
languageName: node
linkType: hard
"lodash.pick@npm:4.4.0, lodash.pick@npm:^4.4.0":
version: 4.4.0
resolution: "lodash.pick@npm:4.4.0"
checksum: 10c0/a04c460b95d1aaa44e9513d1dacf72ea74d838da843e45831de9de64c303f13cdde1859702a6f4dcef417816898ffd47c6ae0614c957ac70245bed2809b8d2e2
languageName: node
linkType: hard
"lodash.pickby@npm:^4.6.0":
version: 4.6.0
resolution: "lodash.pickby@npm:4.6.0"
@@ -51872,7 +51856,6 @@ __metadata:
lodash.merge: "npm:^4.6.2"
lodash.omit: "npm:4.5.0"
lodash.omitby: "npm:^4.6.0"
lodash.pick: "npm:4.4.0"
lodash.snakecase: "npm:4.1.1"
lodash.uniq: "npm:^4.5.0"
lodash.uniqby: "npm:^4.7.0"
@@ -52107,7 +52090,6 @@ __metadata:
"@types/lodash.kebabcase": "npm:^4.1.7"
"@types/lodash.mapvalues": "npm:^4.6.9"
"@types/lodash.omit": "npm:^4.5.9"
"@types/lodash.pick": "npm:^4.3.7"
"@types/lodash.pickby": "npm:^4.6.9"
"@types/lodash.snakecase": "npm:^4.1.7"
"@types/lodash.upperfirst": "npm:^4.3.7"
@@ -52184,7 +52166,6 @@ __metadata:
lodash.mapvalues: "npm:^4.6.0"
lodash.merge: "npm:^4.6.2"
lodash.omit: "npm:^4.5.0"
lodash.pick: "npm:^4.4.0"
lodash.pickby: "npm:^4.6.0"
lodash.snakecase: "npm:^4.1.1"
lodash.upperfirst: "npm:^4.3.1"