From 59169ba135fcbc465a7d552c4e2b2670a7ccf9b7 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 14 Mar 2026 15:04:25 +0100 Subject: [PATCH 1/2] fix --- package-lock.json | 30 ------ package.json | 1 - spec/vulnerabilities.spec.js | 114 +++++++++++++++++++-- src/Controllers/DatabaseController.js | 4 +- src/Controllers/SchemaController.js | 4 +- src/GraphQL/loaders/functionsMutations.js | 4 +- src/GraphQL/loaders/parseClassMutations.js | 8 +- src/GraphQL/loaders/parseClassQueries.js | 6 +- src/GraphQL/loaders/schemaMutations.js | 8 +- src/GraphQL/loaders/schemaQueries.js | 4 +- src/GraphQL/loaders/usersMutations.js | 8 +- src/LiveQuery/ParseLiveQueryServer.ts | 4 +- src/Push/PushWorker.js | 4 +- src/Push/utils.js | 6 +- src/RestWrite.js | 15 ++- 15 files changed, 142 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb8336d301..08466faa82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "bcryptjs": "3.0.3", "commander": "14.0.3", "cors": "2.8.6", - "deepcopy": "2.1.0", "express": "5.2.1", "express-rate-limit": "8.2.1", "follow-redirects": "1.15.9", @@ -9172,14 +9171,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/deepcopy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/deepcopy/-/deepcopy-2.1.0.tgz", - "integrity": "sha512-8cZeTb1ZKC3bdSCP6XOM1IsTczIO73fdqtwa2B0N15eAz7gmyhQo+mc5gnFuulsgN3vIQYmTgbmQVKalH1dKvQ==", - "dependencies": { - "type-detect": "^4.0.8" - } - }, "node_modules/default-require-extensions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", @@ -21776,14 +21767,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.21.0.tgz", @@ -29165,14 +29148,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "deepcopy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/deepcopy/-/deepcopy-2.1.0.tgz", - "integrity": "sha512-8cZeTb1ZKC3bdSCP6XOM1IsTczIO73fdqtwa2B0N15eAz7gmyhQo+mc5gnFuulsgN3vIQYmTgbmQVKalH1dKvQ==", - "requires": { - "type-detect": "^4.0.8" - } - }, "default-require-extensions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", @@ -37917,11 +37892,6 @@ "prelude-ls": "^1.2.1" } }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" - }, "type-fest": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.21.0.tgz", diff --git a/package.json b/package.json index 754867e000..212fad9289 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "bcryptjs": "3.0.3", "commander": "14.0.3", "cors": "2.8.6", - "deepcopy": "2.1.0", "express": "5.2.1", "express-rate-limit": "8.2.1", "follow-redirects": "1.15.9", diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index 39a04180c6..b9e4042761 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -353,12 +353,23 @@ describe('Vulnerabilities', () => { }); it('denies __proto__ after a sibling nested object', async () => { - // Cannot test via HTTP because deepcopy() strips __proto__ before the denylist - // check runs. Test objectContainsKeyValue directly with a JSON.parse'd object - // that preserves __proto__ as an own property. - const Utils = require('../lib/Utils'); - const data = JSON.parse('{"profile": {"name": "alice"}, "__proto__": {"isAdmin": true}}'); - expect(Utils.objectContainsKeyValue(data, '__proto__', undefined)).toBe(true); + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const response = await request({ + headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/PP', + body: JSON.stringify( + JSON.parse('{"profile": {"name": "alice"}, "__proto__": {"isAdmin": true}}') + ), + }).catch(e => e); + expect(response.status).toBe(400); + const text = typeof response.data === 'string' ? JSON.parse(response.data) : response.data; + expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME); + expect(text.error).toContain('__proto__'); }); it('denies constructor after a sibling nested object', async () => { @@ -2644,4 +2655,95 @@ describe('(GHSA-c442-97qw-j6c6) SQL Injection via $regex query operator field na expect(result.body.errors[0].message).toMatch(/exceeds maximum allowed/); }); }); + + describe('(GHSA-9ccr-fpp6-78qf) Schema poisoning via __proto__ bypassing requestKeywordDenylist and addField CLP', () => { + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + + it('rejects __proto__ in request body via HTTP', async () => { + const response = await request({ + headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/ProtoTest', + body: JSON.stringify(JSON.parse('{"name":"test","__proto__":{"injected":"value"}}')), + }).catch(e => e); + expect(response.status).toBe(400); + const text = typeof response.data === 'string' ? JSON.parse(response.data) : response.data; + expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME); + expect(text.error).toContain('__proto__'); + }); + + it('does not add fields to a locked schema via __proto__', async () => { + const schema = new Parse.Schema('LockedSchema'); + schema.addString('name'); + schema.setCLP({ + find: { '*': true }, + get: { '*': true }, + create: { '*': true }, + update: { '*': true }, + delete: { '*': true }, + addField: {}, + }); + await schema.save(); + + // Attempt to inject a field via __proto__ + const response = await request({ + headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/LockedSchema', + body: JSON.stringify(JSON.parse('{"name":"test","__proto__":{"newField":"bypassed"}}')), + }).catch(e => e); + + // Should be rejected by denylist + expect(response.status).toBe(400); + + // Verify schema was not modified + const schemaResponse = await request({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + method: 'GET', + url: 'http://localhost:8378/1/schemas/LockedSchema', + }); + const fields = schemaResponse.data.fields; + expect(fields.newField).toBeUndefined(); + }); + + it('does not cause schema type conflict via __proto__', async () => { + const schema = new Parse.Schema('TypeConflict'); + schema.addString('name'); + schema.addString('score'); + schema.setCLP({ + find: { '*': true }, + get: { '*': true }, + create: { '*': true }, + update: { '*': true }, + delete: { '*': true }, + addField: {}, + }); + await schema.save(); + + // Attempt to inject 'score' as Number via __proto__ + const response = await request({ + headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/TypeConflict', + body: JSON.stringify(JSON.parse('{"name":"test","__proto__":{"score":42}}')), + }).catch(e => e); + + // Should be rejected by denylist + expect(response.status).toBe(400); + + // Verify 'score' field is still String type + const obj = new Parse.Object('TypeConflict'); + obj.set('name', 'valid'); + obj.set('score', 'string-value'); + await obj.save(); + expect(obj.get('score')).toBe('string-value'); + }); + }); }); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 8ee9d8d01a..541be63076 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -8,8 +8,6 @@ import { Parse } from 'parse/node'; import _ from 'lodash'; // @flow-disable-next import intersect from 'intersect'; -// @flow-disable-next -import deepcopy from 'deepcopy'; import logger from '../logger'; import Utils from '../Utils'; import * as SchemaController from './SchemaController'; @@ -541,7 +539,7 @@ class DatabaseController { const originalQuery = query; const originalUpdate = update; // Make a copy of the object, so we don't mutate the incoming data. - update = deepcopy(update); + update = structuredClone(update); var relationUpdates = []; var isMaster = acl === undefined; var aclGroup = acl || []; diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index b605fba632..e112480e65 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -21,8 +21,6 @@ import SchemaCache from '../Adapters/Cache/SchemaCache'; import DatabaseController from './DatabaseController'; import Config from '../Config'; import { createSanitizedError } from '../Error'; -// @flow-disable-next -import deepcopy from 'deepcopy'; import type { Schema, SchemaFields, @@ -573,7 +571,7 @@ class SchemaData { if (!this.__data[schema.className]) { const data = {}; data.fields = injectDefaultSchema(schema).fields; - data.classLevelPermissions = deepcopy(schema.classLevelPermissions); + data.classLevelPermissions = structuredClone(schema.classLevelPermissions); data.indexes = schema.indexes; const classProtectedFields = this.__protectedFields[schema.className]; diff --git a/src/GraphQL/loaders/functionsMutations.js b/src/GraphQL/loaders/functionsMutations.js index 8eae5b2072..0ad7b52dff 100644 --- a/src/GraphQL/loaders/functionsMutations.js +++ b/src/GraphQL/loaders/functionsMutations.js @@ -1,5 +1,5 @@ import { GraphQLNonNull, GraphQLEnumType } from 'graphql'; -import deepcopy from 'deepcopy'; + import { mutationWithClientMutationId } from 'graphql-relay'; import { FunctionsRouter } from '../../Routers/FunctionsRouter'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; @@ -44,7 +44,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context) => { try { - const { functionName, params } = deepcopy(args); + const { functionName, params } = structuredClone(args); const { config, auth, info } = context; return { diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 1c733b6a1f..6c1dce5c38 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -1,7 +1,7 @@ import { GraphQLNonNull } from 'graphql'; import { fromGlobalId, mutationWithClientMutationId } from 'graphql-relay'; import getFieldNames from 'graphql-list-fields'; -import deepcopy from 'deepcopy'; + import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import { extractKeysAndInclude, getParseClassMutationConfig } from '../parseGraphQLUtils'; import * as objectsMutations from '../helpers/objectsMutations'; @@ -75,7 +75,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG }, mutateAndGetPayload: async (args, context, mutationInfo) => { try { - let { fields } = deepcopy(args); + let { fields } = structuredClone(args); if (!fields) { fields = {}; } const { config, auth, info } = context; @@ -178,7 +178,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG }, mutateAndGetPayload: async (args, context, mutationInfo) => { try { - let { id, fields } = deepcopy(args); + let { id, fields } = structuredClone(args); if (!fields) { fields = {}; } const { config, auth, info } = context; @@ -284,7 +284,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG }, mutateAndGetPayload: async (args, context, mutationInfo) => { try { - let { id } = deepcopy(args); + let { id } = structuredClone(args); const { config, auth, info } = context; const globalIdObject = fromGlobalId(id); diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index edf210ace3..466608183a 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -1,7 +1,7 @@ import { GraphQLNonNull } from 'graphql'; import { fromGlobalId } from 'graphql-relay'; import getFieldNames from 'graphql-list-fields'; -import deepcopy from 'deepcopy'; + import pluralize from 'pluralize'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from '../helpers/objectsQueries'; @@ -75,7 +75,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG return await getQuery( parseClass, _source, - deepcopy(args), + structuredClone(args), context, queryInfo, parseGraphQLSchema.parseClasses @@ -99,7 +99,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG async resolve(_source, args, context, queryInfo) { try { // Deep copy args to avoid internal re assign issue - const { where, order, skip, first, after, last, before, options } = deepcopy(args); + const { where, order, skip, first, after, last, before, options } = structuredClone(args); const { readPreference, includeReadPreference, subqueryReadPreference } = options || {}; const { config, auth, info } = context; const selectedFields = getFieldNames(queryInfo); diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index 93cd89d54a..7f7f98bdb2 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -1,6 +1,6 @@ import Parse from 'parse/node'; import { GraphQLNonNull } from 'graphql'; -import deepcopy from 'deepcopy'; + import { mutationWithClientMutationId } from 'graphql-relay'; import * as schemaTypes from './schemaTypes'; import { transformToParse, transformToGraphQL } from '../transformers/schemaFields'; @@ -28,7 +28,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context) => { try { - const { name, schemaFields } = deepcopy(args); + const { name, schemaFields } = structuredClone(args); const { config, auth } = context; enforceMasterKeyAccess(auth, config); @@ -78,7 +78,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context) => { try { - const { name, schemaFields } = deepcopy(args); + const { name, schemaFields } = structuredClone(args); const { config, auth } = context; enforceMasterKeyAccess(auth, config); @@ -130,7 +130,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context) => { try { - const { name } = deepcopy(args); + const { name } = structuredClone(args); const { config, auth } = context; enforceMasterKeyAccess(auth, config); diff --git a/src/GraphQL/loaders/schemaQueries.js b/src/GraphQL/loaders/schemaQueries.js index 2956b47934..2beb23f719 100644 --- a/src/GraphQL/loaders/schemaQueries.js +++ b/src/GraphQL/loaders/schemaQueries.js @@ -1,5 +1,5 @@ import Parse from 'parse/node'; -import deepcopy from 'deepcopy'; + import { GraphQLNonNull, GraphQLList } from 'graphql'; import { transformToGraphQL } from '../transformers/schemaFields'; import * as schemaTypes from './schemaTypes'; @@ -28,7 +28,7 @@ const load = parseGraphQLSchema => { type: new GraphQLNonNull(schemaTypes.CLASS), resolve: async (_source, args, context) => { try { - const { name } = deepcopy(args); + const { name } = structuredClone(args); const { config, auth } = context; enforceMasterKeyAccess(auth, config); diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index 2f59081a03..178655f167 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -1,6 +1,6 @@ import { GraphQLNonNull, GraphQLString, GraphQLBoolean, GraphQLInputObjectType } from 'graphql'; import { mutationWithClientMutationId } from 'graphql-relay'; -import deepcopy from 'deepcopy'; + import UsersRouter from '../../Routers/UsersRouter'; import * as objectsMutations from '../helpers/objectsMutations'; import { OBJECT } from './defaultGraphQLTypes'; @@ -32,7 +32,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context, mutationInfo) => { try { - const { fields } = deepcopy(args); + const { fields } = structuredClone(args); const { config, auth, info } = context; const parseFields = await transformTypes('create', fields, { @@ -109,7 +109,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context, mutationInfo) => { try { - const { fields, authData } = deepcopy(args); + const { fields, authData } = structuredClone(args); const { config, auth, info } = context; const parseFields = await transformTypes('create', fields, { @@ -173,7 +173,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context, mutationInfo) => { try { - const { username, password, authData } = deepcopy(args); + const { username, password, authData } = structuredClone(args); const { config, auth, info } = context; const { sessionToken, objectId, authDataResponse } = ( diff --git a/src/LiveQuery/ParseLiveQueryServer.ts b/src/LiveQuery/ParseLiveQueryServer.ts index 99f1fe2d21..a83b8e1a62 100644 --- a/src/LiveQuery/ParseLiveQueryServer.ts +++ b/src/LiveQuery/ParseLiveQueryServer.ts @@ -25,7 +25,7 @@ import { LRUCache as LRU } from 'lru-cache'; import UserRouter from '../Routers/UsersRouter'; import DatabaseController from '../Controllers/DatabaseController'; import { isDeepStrictEqual } from 'util'; -import deepcopy from 'deepcopy'; + class ParseLiveQueryServer { server: any; @@ -585,7 +585,7 @@ class ParseLiveQueryServer { if (!parseObject) { return false; } - return matchesQuery(deepcopy(parseObject), subscription.query); + return matchesQuery(structuredClone(parseObject), subscription.query); } async _clearCachedRoles(userId: string) { diff --git a/src/Push/PushWorker.js b/src/Push/PushWorker.js index 2b3c4a2fb7..6ffd960f33 100644 --- a/src/Push/PushWorker.js +++ b/src/Push/PushWorker.js @@ -1,6 +1,4 @@ // @flow -// @flow-disable-next -import deepcopy from 'deepcopy'; import AdaptableController from '../Controllers/AdaptableController'; import { master } from '../Auth'; import Config from '../Config'; @@ -91,7 +89,7 @@ export class PushWorker { // Map the on the badges count and return the send result const promises = Object.keys(badgeInstallationsMap).map(badge => { - const payload = deepcopy(body); + const payload = structuredClone(body); payload.data.badge = parseInt(badge); const installations = badgeInstallationsMap[badge]; return this.sendToAdapter(payload, installations, pushStatus, config, UTCOffset); diff --git a/src/Push/utils.js b/src/Push/utils.js index 5be13c272e..4437cc8099 100644 --- a/src/Push/utils.js +++ b/src/Push/utils.js @@ -1,5 +1,5 @@ import Parse from 'parse/node'; -import deepcopy from 'deepcopy'; + export function isPushIncrementing(body) { if (!body.data || !body.data.badge) { @@ -45,7 +45,7 @@ export function transformPushBodyForLocale(body, locale) { if (!data) { return body; } - body = deepcopy(body); + body = structuredClone(body); localizableKeys.forEach(key => { const localeValue = body.data[`${key}-${locale}`]; if (localeValue) { @@ -128,7 +128,7 @@ export function validatePushType(where = {}, validPushTypes = []) { } export function applyDeviceTokenExists(where) { - where = deepcopy(where); + where = structuredClone(where); if (!Object.prototype.hasOwnProperty.call(where, 'deviceToken')) { where['deviceToken'] = { $exists: true }; } diff --git a/src/RestWrite.js b/src/RestWrite.js index b40d8d619b..937f8644e9 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -3,7 +3,6 @@ // This could be either a "create" or an "update". var SchemaController = require('./Controllers/SchemaController'); -var deepcopy = require('deepcopy'); const Auth = require('./Auth'); const Utils = require('./Utils'); @@ -75,8 +74,8 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK // Processing this operation may mutate our data, so we operate on a // copy - this.query = deepcopy(query); - this.data = deepcopy(data); + this.query = structuredClone(query); + this.data = structuredClone(data); // We never change originalData, so we do not need a deep copy this.originalData = originalData; @@ -377,10 +376,10 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function () { JSON.stringify(schema.classLevelPermissions.ACL) !== JSON.stringify({ '*': { read: true, write: true } }) ) { - const acl = deepcopy(schema.classLevelPermissions.ACL); + const acl = structuredClone(schema.classLevelPermissions.ACL); if (acl.currentUser) { if (this.auth.user?.id) { - acl[this.auth.user?.id] = deepcopy(acl.currentUser); + acl[this.auth.user?.id] = structuredClone(acl.currentUser); } delete acl.currentUser; } @@ -617,7 +616,7 @@ RestWrite.prototype.handleAuthData = async function (authData) { // Run beforeLogin hook before storing any updates // to authData on the db; changes to userResult // will be ignored. - await this.runBeforeLoginTrigger(deepcopy(userResult)); + await this.runBeforeLoginTrigger(structuredClone(userResult)); // If we are in login operation via authData // we need to be sure that the user has provided @@ -1788,7 +1787,7 @@ RestWrite.prototype.sanitizedData = function () { delete data[key]; } return data; - }, deepcopy(this.data)); + }, structuredClone(this.data)); return Parse._decode(undefined, data); }; @@ -1838,7 +1837,7 @@ RestWrite.prototype.buildParseObjects = function () { delete data[key]; } return data; - }, deepcopy(this.data)); + }, structuredClone(this.data)); const sanitized = this.sanitizedData(); for (const attribute of readOnlyAttributes) { From 1de3fa4b838e53b07548987f818ccc9e4f227be6 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 14 Mar 2026 15:45:03 +0100 Subject: [PATCH 2/2] fix --- src/GraphQL/loaders/functionsMutations.js | 3 ++- src/GraphQL/loaders/parseClassMutations.js | 8 ++++---- src/GraphQL/loaders/parseClassQueries.js | 6 +++--- src/GraphQL/loaders/schemaMutations.js | 8 ++++---- src/GraphQL/loaders/schemaQueries.js | 4 ++-- src/GraphQL/loaders/usersMutations.js | 7 ++++--- src/GraphQL/parseGraphQLUtils.js | 8 ++++++++ 7 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/GraphQL/loaders/functionsMutations.js b/src/GraphQL/loaders/functionsMutations.js index 0ad7b52dff..c6761d7966 100644 --- a/src/GraphQL/loaders/functionsMutations.js +++ b/src/GraphQL/loaders/functionsMutations.js @@ -1,6 +1,7 @@ import { GraphQLNonNull, GraphQLEnumType } from 'graphql'; import { mutationWithClientMutationId } from 'graphql-relay'; +import { cloneArgs } from '../parseGraphQLUtils'; import { FunctionsRouter } from '../../Routers/FunctionsRouter'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; @@ -44,7 +45,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context) => { try { - const { functionName, params } = structuredClone(args); + const { functionName, params } = cloneArgs(args); const { config, auth, info } = context; return { diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 6c1dce5c38..df9a096995 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -3,7 +3,7 @@ import { fromGlobalId, mutationWithClientMutationId } from 'graphql-relay'; import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; -import { extractKeysAndInclude, getParseClassMutationConfig } from '../parseGraphQLUtils'; +import { extractKeysAndInclude, getParseClassMutationConfig, cloneArgs } from '../parseGraphQLUtils'; import * as objectsMutations from '../helpers/objectsMutations'; import * as objectsQueries from '../helpers/objectsQueries'; import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; @@ -75,7 +75,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG }, mutateAndGetPayload: async (args, context, mutationInfo) => { try { - let { fields } = structuredClone(args); + let { fields } = cloneArgs(args); if (!fields) { fields = {}; } const { config, auth, info } = context; @@ -178,7 +178,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG }, mutateAndGetPayload: async (args, context, mutationInfo) => { try { - let { id, fields } = structuredClone(args); + let { id, fields } = cloneArgs(args); if (!fields) { fields = {}; } const { config, auth, info } = context; @@ -284,7 +284,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG }, mutateAndGetPayload: async (args, context, mutationInfo) => { try { - let { id } = structuredClone(args); + let { id } = cloneArgs(args); const { config, auth, info } = context; const globalIdObject = fromGlobalId(id); diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index 466608183a..6c34d320c6 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -7,7 +7,7 @@ import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from '../helpers/objectsQueries'; import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; import { transformClassNameToGraphQL } from '../transformers/className'; -import { extractKeysAndInclude } from '../parseGraphQLUtils'; +import { extractKeysAndInclude, cloneArgs } from '../parseGraphQLUtils'; const getParseClassQueryConfig = function (parseClassConfig: ?ParseGraphQLClassConfig) { return (parseClassConfig && parseClassConfig.query) || {}; @@ -75,7 +75,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG return await getQuery( parseClass, _source, - structuredClone(args), + cloneArgs(args), context, queryInfo, parseGraphQLSchema.parseClasses @@ -99,7 +99,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG async resolve(_source, args, context, queryInfo) { try { // Deep copy args to avoid internal re assign issue - const { where, order, skip, first, after, last, before, options } = structuredClone(args); + const { where, order, skip, first, after, last, before, options } = cloneArgs(args); const { readPreference, includeReadPreference, subqueryReadPreference } = options || {}; const { config, auth, info } = context; const selectedFields = getFieldNames(queryInfo); diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index 7f7f98bdb2..69c8e1c54c 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -4,7 +4,7 @@ import { GraphQLNonNull } from 'graphql'; import { mutationWithClientMutationId } from 'graphql-relay'; import * as schemaTypes from './schemaTypes'; import { transformToParse, transformToGraphQL } from '../transformers/schemaFields'; -import { enforceMasterKeyAccess } from '../parseGraphQLUtils'; +import { enforceMasterKeyAccess, cloneArgs } from '../parseGraphQLUtils'; import { getClass } from './schemaQueries'; import { createSanitizedError } from '../../Error'; @@ -28,7 +28,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context) => { try { - const { name, schemaFields } = structuredClone(args); + const { name, schemaFields } = cloneArgs(args); const { config, auth } = context; enforceMasterKeyAccess(auth, config); @@ -78,7 +78,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context) => { try { - const { name, schemaFields } = structuredClone(args); + const { name, schemaFields } = cloneArgs(args); const { config, auth } = context; enforceMasterKeyAccess(auth, config); @@ -130,7 +130,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context) => { try { - const { name } = structuredClone(args); + const { name } = cloneArgs(args); const { config, auth } = context; enforceMasterKeyAccess(auth, config); diff --git a/src/GraphQL/loaders/schemaQueries.js b/src/GraphQL/loaders/schemaQueries.js index 2beb23f719..6e16a54bfb 100644 --- a/src/GraphQL/loaders/schemaQueries.js +++ b/src/GraphQL/loaders/schemaQueries.js @@ -3,7 +3,7 @@ import Parse from 'parse/node'; import { GraphQLNonNull, GraphQLList } from 'graphql'; import { transformToGraphQL } from '../transformers/schemaFields'; import * as schemaTypes from './schemaTypes'; -import { enforceMasterKeyAccess } from '../parseGraphQLUtils'; +import { enforceMasterKeyAccess, cloneArgs } from '../parseGraphQLUtils'; const getClass = async (name, schema) => { try { @@ -28,7 +28,7 @@ const load = parseGraphQLSchema => { type: new GraphQLNonNull(schemaTypes.CLASS), resolve: async (_source, args, context) => { try { - const { name } = structuredClone(args); + const { name } = cloneArgs(args); const { config, auth } = context; enforceMasterKeyAccess(auth, config); diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index 178655f167..1067035b93 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -1,6 +1,7 @@ import { GraphQLNonNull, GraphQLString, GraphQLBoolean, GraphQLInputObjectType } from 'graphql'; import { mutationWithClientMutationId } from 'graphql-relay'; +import { cloneArgs } from '../parseGraphQLUtils'; import UsersRouter from '../../Routers/UsersRouter'; import * as objectsMutations from '../helpers/objectsMutations'; import { OBJECT } from './defaultGraphQLTypes'; @@ -32,7 +33,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context, mutationInfo) => { try { - const { fields } = structuredClone(args); + const { fields } = cloneArgs(args); const { config, auth, info } = context; const parseFields = await transformTypes('create', fields, { @@ -109,7 +110,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context, mutationInfo) => { try { - const { fields, authData } = structuredClone(args); + const { fields, authData } = cloneArgs(args); const { config, auth, info } = context; const parseFields = await transformTypes('create', fields, { @@ -173,7 +174,7 @@ const load = parseGraphQLSchema => { }, mutateAndGetPayload: async (args, context, mutationInfo) => { try { - const { username, password, authData } = structuredClone(args); + const { username, password, authData } = cloneArgs(args); const { config, auth, info } = context; const { sessionToken, objectId, authDataResponse } = ( diff --git a/src/GraphQL/parseGraphQLUtils.js b/src/GraphQL/parseGraphQLUtils.js index ba5fd1b416..a7e92405ec 100644 --- a/src/GraphQL/parseGraphQLUtils.js +++ b/src/GraphQL/parseGraphQLUtils.js @@ -55,3 +55,11 @@ export const extractKeysAndInclude = selectedFields => { export const getParseClassMutationConfig = function (parseClassConfig) { return (parseClassConfig && parseClassConfig.mutation) || {}; }; + +export function cloneArgs(args) { + try { + return structuredClone(args); + } catch { + return JSON.parse(JSON.stringify(args)); + } +}