From a3106962ad239f59db4846db8f753d4a7172172d Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Tue, 18 Nov 2025 20:50:42 +0100 Subject: [PATCH 01/13] feat: sanitize error for security --- spec/AudienceRouter.spec.js | 10 ++--- spec/LogsRouter.spec.js | 2 +- spec/ParseAPI.spec.js | 2 +- spec/ParseFile.spec.js | 6 +-- spec/ParseGlobalConfig.spec.js | 2 +- spec/ParseGraphQLServer.spec.js | 14 +++---- spec/ParseInstallation.spec.js | 2 +- spec/ParseQuery.Aggregate.spec.js | 2 +- spec/ParseUser.spec.js | 6 +-- spec/RestQuery.spec.js | 6 +-- spec/Schema.spec.js | 10 ++--- spec/features.spec.js | 2 +- spec/rest.spec.js | 24 +++++------ spec/schemas.spec.js | 20 ++++----- spec/vulnerabilities.spec.js | 2 +- src/Controllers/SchemaController.js | 26 +++++------- src/GraphQL/loaders/schemaMutations.js | 20 +++++---- src/GraphQL/loaders/schemaQueries.js | 4 +- src/GraphQL/parseGraphQLUtils.js | 11 ++++- src/RestQuery.js | 21 +++++++--- src/RestWrite.js | 19 ++++++--- src/Routers/ClassesRouter.js | 6 ++- src/Routers/FilesRouter.js | 7 +++- src/Routers/GlobalConfigRouter.js | 8 +++- src/Routers/GraphQLRouter.js | 8 +++- src/Routers/PurgeRouter.js | 8 +++- src/Routers/PushRouter.js | 8 +++- src/Routers/SchemasRouter.js | 20 ++++++--- src/Routers/UsersRouter.js | 9 +++- src/SecurityError.js | 57 ++++++++++++++++++++++++++ src/SharedRest.js | 21 ++++++---- src/middlewares.js | 13 +++--- src/rest.js | 22 ++++++---- src/triggers.js | 24 +++++++++-- 34 files changed, 284 insertions(+), 138 deletions(-) create mode 100644 src/SecurityError.js diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js index 1525147a40..472cd33990 100644 --- a/spec/AudienceRouter.spec.js +++ b/spec/AudienceRouter.spec.js @@ -269,7 +269,7 @@ describe('AudiencesRouter', () => { }).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); done(); } ); @@ -279,7 +279,7 @@ describe('AudiencesRouter', () => { Parse._request('GET', 'push_audiences', {}).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); done(); } ); @@ -289,7 +289,7 @@ describe('AudiencesRouter', () => { Parse._request('GET', `push_audiences/someId`, {}).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); done(); } ); @@ -301,7 +301,7 @@ describe('AudiencesRouter', () => { }).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); done(); } ); @@ -311,7 +311,7 @@ describe('AudiencesRouter', () => { Parse._request('DELETE', `push_audiences/someId`, {}).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); done(); } ); diff --git a/spec/LogsRouter.spec.js b/spec/LogsRouter.spec.js index b25ac25be5..e2e91002ba 100644 --- a/spec/LogsRouter.spec.js +++ b/spec/LogsRouter.spec.js @@ -61,7 +61,7 @@ describe_only(() => { }).then(fail, response => { const body = response.data; expect(response.status).toEqual(403); - expect(body.error).toEqual('unauthorized: master key is required'); + expect(body.error).toEqual('Permission denied'); done(); }); }); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 6edfa79109..9412353684 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1724,7 +1724,7 @@ describe('miscellaneous', () => { fail('Should not succeed'); }) .catch(response => { - expect(response.data.error).toEqual('unauthorized: master key is required'); + expect(response.data.error).toEqual('Permission denied'); done(); }); }); diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index d6539b7336..9a8eb7840e 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -156,7 +156,7 @@ describe('Parse.File testing', () => { }).then(fail, response => { const del_b = response.data; expect(response.status).toEqual(403); - expect(del_b.error).toMatch(/unauthorized/); + expect(del_b.error).toBe('Permission denied'); // incorrect X-Parse-Master-Key header request({ method: 'DELETE', @@ -169,7 +169,7 @@ describe('Parse.File testing', () => { }).then(fail, response => { const del_b2 = response.data; expect(response.status).toEqual(403); - expect(del_b2.error).toMatch(/unauthorized/); + expect(del_b2.error).toBe('Permission denied'); done(); }); }); @@ -760,7 +760,7 @@ describe('Parse.File testing', () => { url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt', }).catch(e => e); expect(res1.status).toBe(403); - expect(res1.data).toEqual({ code: 119, error: 'Invalid application ID.' }); + expect(res1.data).toEqual({ code: 119, error: 'Permission denied' }); // Ensure server did not crash const res2 = await request({ url: 'http://localhost:8378/1/health' }); expect(res2.status).toEqual(200); diff --git a/spec/ParseGlobalConfig.spec.js b/spec/ParseGlobalConfig.spec.js index e6719433ff..e85d252380 100644 --- a/spec/ParseGlobalConfig.spec.js +++ b/spec/ParseGlobalConfig.spec.js @@ -233,7 +233,7 @@ describe('a GlobalConfig', () => { }).then(fail, response => { const body = response.data; expect(response.status).toEqual(403); - expect(body.error).toEqual('unauthorized: master key is required'); + expect(body.error).toEqual('Permission denied'); done(); }); }); diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 5031a9cdff..6689e09905 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -3501,7 +3501,7 @@ describe('ParseGraphQLServer', () => { fail('should fail'); } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required'); + expect(e.graphQLErrors[0].message).toEqual('Permission denied'); } }); @@ -3871,7 +3871,7 @@ describe('ParseGraphQLServer', () => { fail('should fail'); } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required'); + expect(e.graphQLErrors[0].message).toEqual('Permission denied'); } }); @@ -4096,7 +4096,7 @@ describe('ParseGraphQLServer', () => { fail('should fail'); } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required'); + expect(e.graphQLErrors[0].message).toEqual('Permission denied'); } }); @@ -4137,7 +4137,7 @@ describe('ParseGraphQLServer', () => { fail('should fail'); } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required'); + expect(e.graphQLErrors[0].message).toEqual('Permission denied'); } }); @@ -4155,7 +4155,7 @@ describe('ParseGraphQLServer', () => { fail('should fail'); } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(e.graphQLErrors[0].message).toEqual('unauthorized: master key is required'); + expect(e.graphQLErrors[0].message).toEqual('Permission denied'); } }); }); @@ -6081,7 +6081,7 @@ describe('ParseGraphQLServer', () => { } await expectAsync(createObject('GraphQLClass')).toBeRejectedWith( - jasmine.stringMatching('Permission denied for action create on class GraphQLClass') + jasmine.stringMatching('Permission denied') ); await expectAsync(createObject('PublicClass')).toBeResolved(); await expectAsync( @@ -6115,7 +6115,7 @@ describe('ParseGraphQLServer', () => { 'X-Parse-Session-Token': user4.getSessionToken(), }) ).toBeRejectedWith( - jasmine.stringMatching('Permission denied for action create on class GraphQLClass') + jasmine.stringMatching('Permission denied') ); await expectAsync( createObject('PublicClass', { diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index c03a727b4a..cfd55eb326 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -176,7 +176,7 @@ describe('Installations', () => { .catch(error => { expect(error.code).toBe(119); expect(error.message).toBe( - "Clients aren't allowed to perform the find operation on the installation collection." + 'Permission denied' ); done(); }); diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index d75658b19e..07a2056b78 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -77,7 +77,7 @@ describe('Parse.Query Aggregate testing', () => { Parse._request('GET', `aggregate/someClass`, {}).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); done(); } ); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index ba34fbf6e9..133d18ebae 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -2661,7 +2661,7 @@ describe('Parse.User testing', () => { }).then(fail, response => { const b = response.data; expect(b.code).toEqual(209); - expect(b.error).toBe('Invalid session token'); + expect(b.error).toBe('Permission denied'); done(); }); }); @@ -3379,7 +3379,7 @@ describe('Parse.User testing', () => { done(); }) .catch(err => { - expect(err.message).toBe("Clients aren't allowed to manually update email verification."); + expect(err.message).toBe('Permission denied'); done(); }); }); @@ -4393,7 +4393,7 @@ describe('login as other user', () => { done(); } catch (err) { expect(err.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(err.data.error).toBe('master key is required'); + expect(err.data.error).toBe('Permission denied'); } const sessionsQuery = new Parse.Query(Parse.Session); diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 7b676da1ea..e88236dbeb 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -165,9 +165,7 @@ describe('rest query', () => { }, err => { expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(err.message).toEqual( - 'This user is not allowed to access ' + 'non-existent class: ClientClassCreation' - ); + expect(err.message).toEqual('Permission denied'); done(); } ); @@ -243,7 +241,7 @@ describe('rest query', () => { expectAsync(new Parse.Query('Test').exists('zip').find()).toBeRejectedWith( new Parse.Error( Parse.Error.OPERATION_FORBIDDEN, - 'This user is not allowed to query zip on class Test' + 'Permission denied' ) ), ]); diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 2192678797..8fa62556fa 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -283,7 +283,7 @@ describe('SchemaController', () => { fail('Class permissions should have rejected this query.'); }, err => { - expect(err.message).toEqual('Permission denied for action count on class Stuff.'); + expect(err.message).toEqual('Permission denied'); done(); } ) @@ -1462,7 +1462,7 @@ describe('Class Level Permissions for requiredAuth', () => { done(); }, e => { - expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); + expect(e.message).toEqual('Permission denied'); done(); } ); @@ -1561,7 +1561,7 @@ describe('Class Level Permissions for requiredAuth', () => { done(); }, e => { - expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); + expect(e.message).toEqual('Permission denied'); done(); } ); @@ -1649,7 +1649,7 @@ describe('Class Level Permissions for requiredAuth', () => { done(); }, e => { - expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); + expect(e.message).toEqual('Permission denied'); done(); } ); @@ -1694,7 +1694,7 @@ describe('Class Level Permissions for requiredAuth', () => { done(); }, e => { - expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); + expect(e.message).toEqual('Permission denied'); done(); } ); diff --git a/spec/features.spec.js b/spec/features.spec.js index f138fe4cf6..84c28fa999 100644 --- a/spec/features.spec.js +++ b/spec/features.spec.js @@ -32,7 +32,7 @@ describe('features', () => { done.fail('The serverInfo request should be rejected without the master key'); } catch (error) { expect(error.status).toEqual(403); - expect(error.data.error).toEqual('unauthorized: master key is required'); + expect(error.data.error).toEqual('Permission denied'); done(); } }); diff --git a/spec/rest.spec.js b/spec/rest.spec.js index 1fff4fad59..1fc64945db 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -324,9 +324,7 @@ describe('rest create', () => { }, err => { expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(err.message).toEqual( - 'This user is not allowed to access ' + 'non-existent class: ClientClassCreation' - ); + expect(err.message).toEqual('Permission denied'); done(); } ); @@ -783,7 +781,7 @@ describe('rest create', () => { const query = new Parse.Query('TestObject'); query.include('pointer'); await expectAsync(query.get(obj2.id)).toBeRejectedWithError( - "Clients aren't allowed to perform the get operation on the _PushStatus collection." + 'Permission denied' ); }); @@ -799,7 +797,7 @@ describe('rest create', () => { const query = new Parse.Query('TestObject'); query.include('globalConfigPointer'); await expectAsync(query.get(obj2.id)).toBeRejectedWithError( - "Clients aren't allowed to perform the get operation on the _GlobalConfig collection." + 'Permission denied' ); }); @@ -953,7 +951,7 @@ describe('read-only masterKey', () => { }).toThrow( new Parse.Error( Parse.Error.OPERATION_FORBIDDEN, - `read-only masterKey isn't allowed to perform the create operation.` + 'Permission denied' ) ); expect(() => { @@ -983,7 +981,7 @@ describe('read-only masterKey', () => { } catch (res) { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe( - "read-only masterKey isn't allowed to perform the create operation." + 'Permission denied' ); } await reconfigureServer(); @@ -1037,7 +1035,7 @@ describe('read-only masterKey', () => { .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.data.error).toBe("read-only masterKey isn't allowed to create a schema."); + expect(res.data.error).toBe('Permission denied'); done(); }); }); @@ -1056,7 +1054,7 @@ describe('read-only masterKey', () => { .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.data.error).toBe("read-only masterKey isn't allowed to create a schema."); + expect(res.data.error).toBe('Permission denied'); done(); }); }); @@ -1075,7 +1073,7 @@ describe('read-only masterKey', () => { .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.data.error).toBe("read-only masterKey isn't allowed to update a schema."); + expect(res.data.error).toBe('Permission denied'); done(); }); }); @@ -1094,7 +1092,7 @@ describe('read-only masterKey', () => { .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.data.error).toBe("read-only masterKey isn't allowed to delete a schema."); + expect(res.data.error).toBe('Permission denied'); done(); }); }); @@ -1113,7 +1111,7 @@ describe('read-only masterKey', () => { .then(done.fail) .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.data.error).toBe("read-only masterKey isn't allowed to update the config."); + expect(res.data.error).toBe('Permission denied'); done(); }); }); @@ -1133,7 +1131,7 @@ describe('read-only masterKey', () => { .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe( - "read-only masterKey isn't allowed to send push notifications." + 'Permission denied' ); done(); }); diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 7891fa847e..c72614a314 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -173,7 +173,7 @@ describe('schemas', () => { headers: restKeyHeaders, }).then(fail, response => { expect(response.status).toEqual(403); - expect(response.data.error).toEqual('unauthorized: master key is required'); + expect(response.data.error).toEqual('Permission denied'); done(); }); }); @@ -185,7 +185,7 @@ describe('schemas', () => { headers: restKeyHeaders, }).then(fail, response => { expect(response.status).toEqual(403); - expect(response.data.error).toEqual('unauthorized: master key is required'); + expect(response.data.error).toEqual('Permission denied'); done(); }); }); @@ -1834,7 +1834,7 @@ describe('schemas', () => { done(); }, err => { - expect(err.message).toEqual('Permission denied for action addField on class AClass.'); + expect(err.message).toEqual('Permission denied'); done(); } ); @@ -2204,7 +2204,7 @@ describe('schemas', () => { fail('Use should hot be able to find!'); }, err => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); + expect(err.message).toEqual('Permission denied'); return Promise.resolve(); } ); @@ -2264,7 +2264,7 @@ describe('schemas', () => { fail('User should not be able to find!'); }, err => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); + expect(err.message).toEqual('Permission denied'); return Promise.resolve(); } ); @@ -2349,7 +2349,7 @@ describe('schemas', () => { fail('User should not be able to find!'); }, err => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); + expect(err.message).toEqual('Permission denied'); return Promise.resolve(); } ); @@ -2425,7 +2425,7 @@ describe('schemas', () => { fail('User should not be able to find!'); }, err => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); + expect(err.message).toEqual('Permission denied'); return Promise.resolve(); } ); @@ -2456,7 +2456,7 @@ describe('schemas', () => { fail('User should not be able to find!'); }, err => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); + expect(err.message).toEqual('Permission denied'); return Promise.resolve(); } ); @@ -2540,7 +2540,7 @@ describe('schemas', () => { return Promise.resolve(); }, err => { - expect(err.message).toEqual('Permission denied for action create on class AClass.'); + expect(err.message).toEqual('Permission denied'); return Promise.resolve(); } ) @@ -2557,7 +2557,7 @@ describe('schemas', () => { return Promise.resolve(); }, err => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); + expect(err.message).toEqual('Permission denied'); return Promise.resolve(); } ) diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index 0d66c0a135..f270400424 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -15,7 +15,7 @@ describe('Vulnerabilities', () => { it('denies user creation with poisoned object ID', async () => { await expectAsync( new Parse.User({ id: 'role:a', username: 'a', password: '123' }).save() - ).toBeRejectedWith(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.')); + ).toBeRejectedWith(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')); }); describe('existing sessions for users with poisoned object ID', () => { diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index fccadd23ce..b56be2bd31 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -20,6 +20,8 @@ import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; import SchemaCache from '../Adapters/Cache/SchemaCache'; import DatabaseController from './DatabaseController'; import Config from '../Config'; +import { createSanitizedError } from '../SecurityError'; +import defaultLogger from '../logger'; // @flow-disable-next import deepcopy from 'deepcopy'; import type { @@ -1403,15 +1405,11 @@ export default class SchemaController { if (perms['requiresAuthentication']) { // If aclGroup has * (public) if (!aclGroup || aclGroup.length == 0) { - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Permission denied, user needs to be authenticated.' - ); + const detailedError = 'Permission denied, user needs to be authenticated.'; + throw createSanitizedError(Parse.Error.OBJECT_NOT_FOUND, detailedError, defaultLogger); } else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) { - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Permission denied, user needs to be authenticated.' - ); + const detailedError = 'Permission denied, user needs to be authenticated.'; + throw createSanitizedError(Parse.Error.OBJECT_NOT_FOUND, detailedError, defaultLogger); } // requiresAuthentication passed, just move forward // probably would be wise at some point to rename to 'authenticatedUser' @@ -1425,10 +1423,8 @@ export default class SchemaController { // Reject create when write lockdown if (permissionField == 'writeUserFields' && operation == 'create') { - throw new Parse.Error( - Parse.Error.OPERATION_FORBIDDEN, - `Permission denied for action ${operation} on class ${className}.` - ); + const detailedError = `Permission denied for action ${operation} on class ${className}.`; + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, defaultLogger); } // Process the readUserFields later @@ -1448,10 +1444,8 @@ export default class SchemaController { } } - throw new Parse.Error( - Parse.Error.OPERATION_FORBIDDEN, - `Permission denied for action ${operation} on class ${className}.` - ); + const detailedError = `Permission denied for action ${operation} on class ${className}.`; + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, defaultLogger); } // Validates an operation passes class-level-permissions set in the schema diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index ffb4d6523b..22dcc090c6 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -6,6 +6,8 @@ import * as schemaTypes from './schemaTypes'; import { transformToParse, transformToGraphQL } from '../transformers/schemaFields'; import { enforceMasterKeyAccess } from '../parseGraphQLUtils'; import { getClass } from './schemaQueries'; +import { createSanitizedError } from '../../SecurityError'; +import defaultLogger from '../../logger'; const load = parseGraphQLSchema => { const createClassMutation = mutationWithClientMutationId({ @@ -30,12 +32,14 @@ const load = parseGraphQLSchema => { const { name, schemaFields } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); if (auth.isReadOnly) { - throw new Parse.Error( + const loggerOrConfig = config || defaultLogger; + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to create a schema." + "read-only masterKey isn't allowed to create a schema.", + loggerOrConfig ); } @@ -79,7 +83,7 @@ const load = parseGraphQLSchema => { const { name, schemaFields } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); if (auth.isReadOnly) { throw new Parse.Error( @@ -130,12 +134,14 @@ const load = parseGraphQLSchema => { const { name } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); if (auth.isReadOnly) { - throw new Parse.Error( + const loggerOrConfig = config || defaultLogger; + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to delete a schema." + "read-only masterKey isn't allowed to delete a schema.", + loggerOrConfig ); } diff --git a/src/GraphQL/loaders/schemaQueries.js b/src/GraphQL/loaders/schemaQueries.js index 25bc071919..2956b47934 100644 --- a/src/GraphQL/loaders/schemaQueries.js +++ b/src/GraphQL/loaders/schemaQueries.js @@ -31,7 +31,7 @@ const load = parseGraphQLSchema => { const { name } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); const schema = await config.database.loadSchema({ clearCache: true }); const parseClass = await getClass(name, schema); @@ -57,7 +57,7 @@ const load = parseGraphQLSchema => { try { const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); const schema = await config.database.loadSchema({ clearCache: true }); return (await schema.getAllClasses(true)).map(parseClass => ({ diff --git a/src/GraphQL/parseGraphQLUtils.js b/src/GraphQL/parseGraphQLUtils.js index f1194784cb..7c272341cf 100644 --- a/src/GraphQL/parseGraphQLUtils.js +++ b/src/GraphQL/parseGraphQLUtils.js @@ -1,9 +1,16 @@ import Parse from 'parse/node'; import { GraphQLError } from 'graphql'; +import { createSanitizedError } from '../SecurityError'; +import defaultLogger from '../logger'; -export function enforceMasterKeyAccess(auth) { +export function enforceMasterKeyAccess(auth, config = null) { if (!auth.isMaster) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'unauthorized: master key is required'); + const loggerOrConfig = config || defaultLogger; + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + 'unauthorized: master key is required', + loggerOrConfig + ); } } diff --git a/src/RestQuery.js b/src/RestQuery.js index c48cecdb6f..d220dd9727 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -7,6 +7,8 @@ const triggers = require('./triggers'); const { continueWhile } = require('parse/lib/node/promiseUtils'); const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL']; const { enforceRoleSecurity } = require('./SharedRest'); +const { createSanitizedError } = require('./SecurityError'); +const defaultLogger = require('./logger').default; // restOptions can include: // skip @@ -51,7 +53,7 @@ async function RestQuery({ throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad query type'); } const isGet = method === RestQuery.Method.get; - enforceRoleSecurity(method, className, auth); + enforceRoleSecurity(method, className, auth, config); const result = runBeforeFind ? await triggers.maybeRunQueryTrigger( triggers.Types.beforeFind, @@ -120,7 +122,13 @@ function _UnsafeRestQuery( if (!this.auth.isMaster) { if (this.className == '_Session') { if (!this.auth.user) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + const detailedError = 'Invalid session token'; + const log = (this.config && this.config.loggerController) || defaultLogger; + throw createSanitizedError( + Parse.Error.INVALID_SESSION_TOKEN, + detailedError, + log + ); } this.restWhere = { $and: [ @@ -421,7 +429,7 @@ _UnsafeRestQuery.prototype.validateClientClassCreation = function () { .then(schemaController => schemaController.hasClass(this.className)) .then(hasClass => { if (hasClass !== true) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, 'This user is not allowed to access ' + 'non-existent class: ' + this.className ); @@ -800,9 +808,12 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () { ) || []; for (const key of protectedFields) { if (this.restWhere[key]) { - throw new Parse.Error( + const detailedError = `This user is not allowed to query ${key} on class ${this.className}`; + const log = (this.config && this.config.loggerController) || defaultLogger; + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - `This user is not allowed to query ${key} on class ${this.className}` + detailedError, + log ); } } diff --git a/src/RestWrite.js b/src/RestWrite.js index 41b6c23468..987f455263 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -17,6 +17,8 @@ import RestQuery from './RestQuery'; import _ from 'lodash'; import logger from './logger'; import { requiredColumns } from './Controllers/SchemaController'; +import { createSanitizedError } from './SecurityError'; +import defaultLogger from './logger'; // query and data are both provided in REST API format. So data // types are encoded by plain old objects. @@ -199,9 +201,12 @@ RestWrite.prototype.validateClientClassCreation = function () { .then(schemaController => schemaController.hasClass(this.className)) .then(hasClass => { if (hasClass !== true) { - throw new Parse.Error( + const detailedError = 'This user is not allowed to access non-existent class: ' + this.className; + const log = (this.config && this.config.loggerController) || defaultLogger; + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - 'This user is not allowed to access ' + 'non-existent class: ' + this.className + detailedError, + log ); } }); @@ -660,8 +665,7 @@ RestWrite.prototype.checkRestrictedFields = async function () { } if (!this.auth.isMaintenance && !this.auth.isMaster && 'emailVerified' in this.data) { - const error = `Clients aren't allowed to manually update email verification.`; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Clients aren\'t allowed to manually update email verification.'); } }; @@ -1450,9 +1454,12 @@ RestWrite.prototype.runDatabaseOperation = function () { } if (this.className === '_User' && this.query && this.auth.isUnauthenticated()) { - throw new Parse.Error( + const detailedError = `Cannot modify user ${this.query.objectId}.`; + const log = (this.config && this.config.loggerController) || defaultLogger; + throw createSanitizedError( Parse.Error.SESSION_MISSING, - `Cannot modify user ${this.query.objectId}.` + detailedError, + log ); } diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 8b6e447757..9797e8837e 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -3,6 +3,8 @@ import rest from '../rest'; import _ from 'lodash'; import Parse from 'parse/node'; import { promiseEnsureIdempotency } from '../middlewares'; +import { createSanitizedError } from '../SecurityError'; +import defaultLogger from '../logger'; const ALLOWED_GET_QUERY_KEYS = [ 'keys', @@ -111,7 +113,9 @@ export class ClassesRouter extends PromiseRouter { typeof req.body?.objectId === 'string' && req.body.objectId.startsWith('role:') ) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.'); + const detailedError = 'Invalid object ID.'; + const log = (req.config && req.config.loggerController) || defaultLogger; + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, log); } return rest.create( req.config, diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 5cb39abf47..c0bd565149 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -5,6 +5,8 @@ import Config from '../Config'; import logger from '../logger'; const triggers = require('../triggers'); const Utils = require('../Utils'); +import { createSanitizedError } from '../SecurityError'; +import defaultLogger from '../logger'; export class FilesRouter { expressRouter({ maxUploadSize = '20Mb' } = {}) { @@ -43,7 +45,10 @@ export class FilesRouter { const config = Config.get(req.params.appId); if (!config) { res.status(403); - const err = new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid application ID.'); + const detailedError = 'Invalid application ID.'; + const log = defaultLogger; + log.error('Security error:', detailedError); + const err = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, log); res.json({ code: err.code, error: err.message }); return; } diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index 5a28b3bae1..ce8b09bbed 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -3,6 +3,8 @@ import Parse from 'parse/node'; import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import * as triggers from '../triggers'; +import { createSanitizedError } from '../SecurityError'; +import defaultLogger from '../logger'; const getConfigFromParams = params => { const config = new Parse.Config(); @@ -41,9 +43,11 @@ export class GlobalConfigRouter extends PromiseRouter { async updateGlobalConfig(req) { if (req.auth.isReadOnly) { - throw new Parse.Error( + const log = (req.config && req.config.loggerController) || defaultLogger; + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to update the config." + "read-only masterKey isn't allowed to update the config.", + log ); } const params = req.body.params || {}; diff --git a/src/Routers/GraphQLRouter.js b/src/Routers/GraphQLRouter.js index d472ac9df5..1571bae9ab 100644 --- a/src/Routers/GraphQLRouter.js +++ b/src/Routers/GraphQLRouter.js @@ -1,6 +1,8 @@ import Parse from 'parse/node'; import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; +import { createSanitizedError } from '../SecurityError'; +import defaultLogger from '../logger'; const GraphQLConfigPath = '/graphql-config'; @@ -14,9 +16,11 @@ export class GraphQLRouter extends PromiseRouter { async updateGraphQLConfig(req) { if (req.auth.isReadOnly) { - throw new Parse.Error( + const log = (req.config && req.config.loggerController) || defaultLogger; + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to update the GraphQL config." + "read-only masterKey isn't allowed to update the GraphQL config.", + log ); } const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {}); diff --git a/src/Routers/PurgeRouter.js b/src/Routers/PurgeRouter.js index 3195d134af..980cde54a5 100644 --- a/src/Routers/PurgeRouter.js +++ b/src/Routers/PurgeRouter.js @@ -1,13 +1,17 @@ import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import Parse from 'parse/node'; +import { createSanitizedError } from '../SecurityError'; +import defaultLogger from '../logger'; export class PurgeRouter extends PromiseRouter { handlePurge(req) { if (req.auth.isReadOnly) { - throw new Parse.Error( + const log = (req.config && req.config.loggerController) || defaultLogger; + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to purge a schema." + "read-only masterKey isn't allowed to purge a schema.", + log ); } return req.config.database diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js index 1c1c8f3b5f..fab3807021 100644 --- a/src/Routers/PushRouter.js +++ b/src/Routers/PushRouter.js @@ -1,6 +1,8 @@ import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import { Parse } from 'parse/node'; +import { createSanitizedError } from '../SecurityError'; +import defaultLogger from '../logger'; export class PushRouter extends PromiseRouter { mountRoutes() { @@ -9,9 +11,11 @@ export class PushRouter extends PromiseRouter { static handlePOST(req) { if (req.auth.isReadOnly) { - throw new Parse.Error( + const log = (req.config && req.config.loggerController) || defaultLogger; + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to send push notifications." + "read-only masterKey isn't allowed to send push notifications.", + log ); } const pushController = req.config.pushController; diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index 0a42123af7..2fdac66f95 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -5,6 +5,8 @@ var Parse = require('parse/node').Parse, import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; +import { createSanitizedError } from '../SecurityError'; +import defaultLogger from '../logger'; function classNameMismatchResponse(bodyClass, pathClass) { throw new Parse.Error( @@ -72,9 +74,11 @@ export const internalUpdateSchema = async (className, body, config) => { async function createSchema(req) { checkIfDefinedSchemasIsUsed(req); if (req.auth.isReadOnly) { - throw new Parse.Error( + const log = (req.config && req.config.loggerController) || defaultLogger; + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to create a schema." + "read-only masterKey isn't allowed to create a schema.", + log ); } if (req.params.className && req.body?.className) { @@ -94,9 +98,11 @@ async function createSchema(req) { function modifySchema(req) { checkIfDefinedSchemasIsUsed(req); if (req.auth.isReadOnly) { - throw new Parse.Error( + const log = (req.config && req.config.loggerController) || defaultLogger; + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to update a schema." + "read-only masterKey isn't allowed to update a schema.", + log ); } if (req.body?.className && req.body.className != req.params.className) { @@ -109,9 +115,11 @@ function modifySchema(req) { const deleteSchema = req => { if (req.auth.isReadOnly) { - throw new Parse.Error( + const log = (req.config && req.config.loggerController) || defaultLogger; + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to delete a schema." + "read-only masterKey isn't allowed to delete a schema.", + log ); } if (!SchemaController.classNameIsValid(req.params.className)) { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 4f38c60b6c..19c7c4bd9e 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -16,6 +16,8 @@ import { import { promiseEnsureIdempotency } from '../middlewares'; import RestWrite from '../RestWrite'; import { logger } from '../logger'; +import { createSanitizedError } from '../SecurityError'; +import defaultLogger from '../logger'; export class UsersRouter extends ClassesRouter { className() { @@ -333,7 +335,12 @@ export class UsersRouter extends ClassesRouter { */ async handleLogInAs(req) { if (!req.auth.isMaster) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'master key is required'); + const log = (req.config && req.config.loggerController) || defaultLogger; + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + 'master key is required', + log + ); } const userId = req.body?.userId || req.query.userId; diff --git a/src/SecurityError.js b/src/SecurityError.js new file mode 100644 index 0000000000..14fda7e8d0 --- /dev/null +++ b/src/SecurityError.js @@ -0,0 +1,57 @@ +import Parse from 'parse/node'; +import defaultLogger from './logger'; + +/** + * Creates a sanitized security error that hides detailed information from clients + * while logging the detailed message server-side. + * + * @param {number} errorCode - The Parse.Error code (e.g., Parse.Error.OPERATION_FORBIDDEN) + * @param {string} detailedMessage - The detailed error message to log server-side + * @param {Object} loggerOrConfig - Optional logger instance or config object (from req.config.loggerController or default) + * @returns {Parse.Error} A Parse.Error with sanitized message + */ +export function createSanitizedError(errorCode, detailedMessage, loggerOrConfig = null) { + let log = defaultLogger; + if (loggerOrConfig) { + if (loggerOrConfig.loggerController) { + log = loggerOrConfig.loggerController; + } else if (loggerOrConfig.error) { + // It's a logger instance + log = loggerOrConfig; + } + } + + // Keep log on server side + log.error('Security error:', detailedMessage); + + return new Parse.Error(errorCode, 'Permission denied'); +} + +/** + * Creates a sanitized security error from a regular Error object + * Used for non-Parse.Error security errors (e.g., Express errors) + * + * @param {number} statusCode - HTTP status code (e.g., 403) + * @param {string} detailedMessage - The detailed error message to log server-side + * @param {Object} loggerOrConfig - Optional logger instance or config object + * @returns {Error} An Error with sanitized message + */ +export function createSanitizedHttpError(statusCode, detailedMessage, loggerOrConfig = null) { + let log = defaultLogger; + if (loggerOrConfig) { + if (loggerOrConfig.loggerController) { + log = loggerOrConfig.loggerController; + } else if (loggerOrConfig.error) { + log = loggerOrConfig; + } + } + + // Keep log on server side + log.error('Security error:', detailedMessage); + + const error = new Error(); + error.status = statusCode; + error.message = 'Permission denied'; + return error; +} + diff --git a/src/SharedRest.js b/src/SharedRest.js index 0b4a07c320..1817d682ce 100644 --- a/src/SharedRest.js +++ b/src/SharedRest.js @@ -1,3 +1,4 @@ +const Parse = require('parse/node'); const classesWithMasterOnlyAccess = [ '_JobStatus', '_PushStatus', @@ -6,12 +7,16 @@ const classesWithMasterOnlyAccess = [ '_JobSchedule', '_Idempotency', ]; +const { createSanitizedError } = require('./SecurityError'); +const defaultLogger = require('./logger').default; + // Disallowing access to the _Role collection except by master key -function enforceRoleSecurity(method, className, auth) { +function enforceRoleSecurity(method, className, auth, config = null) { if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) { if (method === 'delete' || method === 'find') { - const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + const detailedError = `Clients aren't allowed to perform the ${method} operation on the installation collection.`; + const loggerOrConfig = config || defaultLogger; + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, loggerOrConfig); } } @@ -21,14 +26,16 @@ function enforceRoleSecurity(method, className, auth) { !auth.isMaster && !auth.isMaintenance ) { - const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + const detailedError = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`; + const loggerOrConfig = config || defaultLogger; + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, loggerOrConfig); } // readOnly masterKey is not allowed if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) { - const error = `read-only masterKey isn't allowed to perform the ${method} operation.`; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + const detailedError = `read-only masterKey isn't allowed to perform the ${method} operation.`; + const loggerOrConfig = config || defaultLogger; + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, loggerOrConfig); } } diff --git a/src/middlewares.js b/src/middlewares.js index 93b16f3846..6e135f047f 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -13,6 +13,7 @@ import { pathToRegexp } from 'path-to-regexp'; import RedisStore from 'rate-limit-redis'; import { createClient } from 'redis'; import { BlockList, isIPv4 } from 'net'; +import { createSanitizedHttpError } from './SecurityError'; export const DEFAULT_ALLOWED_HEADERS = 'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, X-Parse-Request-Id, Content-Type, Pragma, Cache-Control'; @@ -501,8 +502,10 @@ export function handleParseErrors(err, req, res, next) { export function enforceMasterKeyAccess(req, res, next) { if (!req.auth.isMaster) { - res.status(403); - res.end('{"error":"unauthorized: master key is required"}'); + const log = (req.config && req.config.loggerController) || defaultLogger; + const error = createSanitizedHttpError(403, 'unauthorized: master key is required', log); + res.status(error.status); + res.end(`{"error":"${error.message}"}`); return; } next(); @@ -510,10 +513,8 @@ export function enforceMasterKeyAccess(req, res, next) { export function promiseEnforceMasterKeyAccess(request) { if (!request.auth.isMaster) { - const error = new Error(); - error.status = 403; - error.message = 'unauthorized: master key is required'; - throw error; + const log = (request.config && request.config.loggerController) || defaultLogger; + throw createSanitizedHttpError(403, 'unauthorized: master key is required', log); } return Promise.resolve(); } diff --git a/src/rest.js b/src/rest.js index e2e688a972..71cd2b045e 100644 --- a/src/rest.js +++ b/src/rest.js @@ -134,7 +134,7 @@ async function runFindTriggers( // Returns a promise for an object with optional keys 'results' and 'count'. const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => { - enforceRoleSecurity('find', className, auth); + enforceRoleSecurity('find', className, auth, config); return runFindTriggers( config, auth, @@ -149,7 +149,7 @@ const find = async (config, auth, className, restWhere, restOptions, clientSDK, // get is just like find but only queries an objectId. const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => { - enforceRoleSecurity('get', className, auth); + enforceRoleSecurity('get', className, auth, config); return runFindTriggers( config, auth, @@ -172,7 +172,7 @@ function del(config, auth, className, objectId, context) { throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth to delete user'); } - enforceRoleSecurity('delete', className, auth); + enforceRoleSecurity('delete', className, auth, config); let inflatedObject; let schemaController; @@ -257,13 +257,13 @@ function del(config, auth, className, objectId, context) { ); }) .catch(error => { - handleSessionMissingError(error, className, auth); + handleSessionMissingError(error, className, auth, config); }); } // Returns a promise for a {response, status, location} object. function create(config, auth, className, restObject, clientSDK, context) { - enforceRoleSecurity('create', className, auth); + enforceRoleSecurity('create', className, auth, config); var write = new RestWrite(config, auth, className, null, restObject, null, clientSDK, context); return write.execute(); } @@ -272,7 +272,7 @@ function create(config, auth, className, restObject, clientSDK, context) { // REST API is supposed to return. // Usually, this is just updatedAt. function update(config, auth, className, restWhere, restObject, clientSDK, context) { - enforceRoleSecurity('update', className, auth); + enforceRoleSecurity('update', className, auth, config); return Promise.resolve() .then(async () => { @@ -314,11 +314,11 @@ function update(config, auth, className, restWhere, restObject, clientSDK, conte ).execute(); }) .catch(error => { - handleSessionMissingError(error, className, auth); + handleSessionMissingError(error, className, auth, config); }); } -function handleSessionMissingError(error, className, auth) { +function handleSessionMissingError(error, className, auth, config = null) { // If we're trying to update a user without / with bad session token if ( className === '_User' && @@ -326,7 +326,11 @@ function handleSessionMissingError(error, className, auth) { !auth.isMaster && !auth.isMaintenance ) { - throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth.'); + const { createSanitizedError } = require('./SecurityError'); + const defaultLogger = require('./logger').default; + const detailedError = 'Insufficient auth.'; + const log = (config && config.loggerController) || defaultLogger; + throw createSanitizedError(Parse.Error.SESSION_MISSING, detailedError, log); } throw error; } diff --git a/src/triggers.js b/src/triggers.js index 26b107f062..4aea52b687 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1,6 +1,8 @@ // triggers.js import Parse from 'parse/node'; import { logger } from './logger'; +import { createSanitizedError } from './SecurityError'; +import defaultLogger from './logger'; export const Types = { beforeLogin: 'beforeLogin', @@ -710,10 +712,24 @@ export function maybeRunValidator(request, functionName, auth) { resolve(); }) .catch(e => { - const error = resolveError(e, { - code: Parse.Error.VALIDATION_ERROR, - message: 'Validation failed.', - }); + // Check if this is a security-related validation error + const errorMessage = typeof e === 'string' ? e : (e.message || String(e)); + const isSecurityError = errorMessage.includes('Master key is required') || errorMessage.includes('Please login to continue'); + + let error; + if (isSecurityError) { + // Log detailed error server-side and sanitize for client + defaultLogger.error('Security validation error:', errorMessage); + error = resolveError(e, { + code: Parse.Error.VALIDATION_ERROR, + message: 'Permission denied', + }); + } else { + error = resolveError(e, { + code: Parse.Error.VALIDATION_ERROR, + message: 'Validation failed.', + }); + } reject(error); }); }); From a413efdfe1a6e3f9464be9cdc1646dad162a3ead Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Tue, 18 Nov 2025 20:56:06 +0100 Subject: [PATCH 02/13] fix: auto review --- src/SecurityError.js | 1 - src/SharedRest.js | 1 - src/triggers.js | 26 +++++--------------------- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/SecurityError.js b/src/SecurityError.js index 14fda7e8d0..df7cf5bdae 100644 --- a/src/SecurityError.js +++ b/src/SecurityError.js @@ -1,4 +1,3 @@ -import Parse from 'parse/node'; import defaultLogger from './logger'; /** diff --git a/src/SharedRest.js b/src/SharedRest.js index 1817d682ce..cc833d7d8c 100644 --- a/src/SharedRest.js +++ b/src/SharedRest.js @@ -1,4 +1,3 @@ -const Parse = require('parse/node'); const classesWithMasterOnlyAccess = [ '_JobStatus', '_PushStatus', diff --git a/src/triggers.js b/src/triggers.js index 4aea52b687..7905fae326 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1,8 +1,6 @@ // triggers.js import Parse from 'parse/node'; import { logger } from './logger'; -import { createSanitizedError } from './SecurityError'; -import defaultLogger from './logger'; export const Types = { beforeLogin: 'beforeLogin', @@ -712,24 +710,10 @@ export function maybeRunValidator(request, functionName, auth) { resolve(); }) .catch(e => { - // Check if this is a security-related validation error - const errorMessage = typeof e === 'string' ? e : (e.message || String(e)); - const isSecurityError = errorMessage.includes('Master key is required') || errorMessage.includes('Please login to continue'); - - let error; - if (isSecurityError) { - // Log detailed error server-side and sanitize for client - defaultLogger.error('Security validation error:', errorMessage); - error = resolveError(e, { - code: Parse.Error.VALIDATION_ERROR, - message: 'Permission denied', - }); - } else { - error = resolveError(e, { - code: Parse.Error.VALIDATION_ERROR, - message: 'Validation failed.', - }); - } + const error = resolveError(e, { + code: Parse.Error.VALIDATION_ERROR, + message: 'Validation failed.', + }); reject(error); }); }); @@ -1125,4 +1109,4 @@ export async function maybeRunGlobalConfigTrigger(triggerType, auth, configObjec } } return configObject; -} +} \ No newline at end of file From d78f9944f7e2e460cc082c241513c67795a2584e Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Tue, 18 Nov 2025 21:53:58 +0100 Subject: [PATCH 03/13] fix: feedbacks --- src/rest.js | 21 ++++++++++----------- src/triggers.js | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/rest.js b/src/rest.js index 71cd2b045e..511363d4c3 100644 --- a/src/rest.js +++ b/src/rest.js @@ -134,7 +134,7 @@ async function runFindTriggers( // Returns a promise for an object with optional keys 'results' and 'count'. const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => { - enforceRoleSecurity('find', className, auth, config); + enforceRoleSecurity('find', className, auth); return runFindTriggers( config, auth, @@ -149,7 +149,7 @@ const find = async (config, auth, className, restWhere, restOptions, clientSDK, // get is just like find but only queries an objectId. const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => { - enforceRoleSecurity('get', className, auth, config); + enforceRoleSecurity('get', className, auth); return runFindTriggers( config, auth, @@ -172,7 +172,7 @@ function del(config, auth, className, objectId, context) { throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth to delete user'); } - enforceRoleSecurity('delete', className, auth, config); + enforceRoleSecurity('delete', className, auth); let inflatedObject; let schemaController; @@ -257,13 +257,13 @@ function del(config, auth, className, objectId, context) { ); }) .catch(error => { - handleSessionMissingError(error, className, auth, config); + handleSessionMissingError(error, className, auth); }); } // Returns a promise for a {response, status, location} object. function create(config, auth, className, restObject, clientSDK, context) { - enforceRoleSecurity('create', className, auth, config); + enforceRoleSecurity('create', className, auth); var write = new RestWrite(config, auth, className, null, restObject, null, clientSDK, context); return write.execute(); } @@ -272,7 +272,7 @@ function create(config, auth, className, restObject, clientSDK, context) { // REST API is supposed to return. // Usually, this is just updatedAt. function update(config, auth, className, restWhere, restObject, clientSDK, context) { - enforceRoleSecurity('update', className, auth, config); + enforceRoleSecurity('update', className, auth); return Promise.resolve() .then(async () => { @@ -314,11 +314,11 @@ function update(config, auth, className, restWhere, restObject, clientSDK, conte ).execute(); }) .catch(error => { - handleSessionMissingError(error, className, auth, config); + handleSessionMissingError(error, className, auth); }); } -function handleSessionMissingError(error, className, auth, config = null) { +function handleSessionMissingError(error, className, auth) { // If we're trying to update a user without / with bad session token if ( className === '_User' && @@ -327,10 +327,9 @@ function handleSessionMissingError(error, className, auth, config = null) { !auth.isMaintenance ) { const { createSanitizedError } = require('./SecurityError'); - const defaultLogger = require('./logger').default; const detailedError = 'Insufficient auth.'; - const log = (config && config.loggerController) || defaultLogger; - throw createSanitizedError(Parse.Error.SESSION_MISSING, detailedError, log); + + throw createSanitizedError(Parse.Error.SESSION_MISSING, detailedError); } throw error; } diff --git a/src/triggers.js b/src/triggers.js index 7905fae326..26b107f062 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1109,4 +1109,4 @@ export async function maybeRunGlobalConfigTrigger(triggerType, auth, configObjec } } return configObject; -} \ No newline at end of file +} From 79bf5f5e5483e5be48772d1a863710a5757f7bb2 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:38:12 +0100 Subject: [PATCH 04/13] fix: feedbacks --- src/Controllers/SchemaController.js | 8 ++++---- src/GraphQL/loaders/schemaMutations.js | 5 ----- src/GraphQL/parseGraphQLUtils.js | 5 +---- src/RestQuery.js | 4 ---- src/RestWrite.js | 2 -- src/Routers/ClassesRouter.js | 3 +-- src/Routers/FilesRouter.js | 5 +---- src/Routers/GlobalConfigRouter.js | 3 --- src/Routers/GraphQLRouter.js | 3 --- src/Routers/PurgeRouter.js | 3 --- src/Routers/PushRouter.js | 3 --- src/Routers/SchemasRouter.js | 7 ------- src/Routers/UsersRouter.js | 3 --- src/SecurityError.js | 28 ++++---------------------- src/SharedRest.js | 12 ++++------- src/middlewares.js | 6 ++---- 16 files changed, 17 insertions(+), 83 deletions(-) diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index b56be2bd31..6ecea6e9b1 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -1406,10 +1406,10 @@ export default class SchemaController { // If aclGroup has * (public) if (!aclGroup || aclGroup.length == 0) { const detailedError = 'Permission denied, user needs to be authenticated.'; - throw createSanitizedError(Parse.Error.OBJECT_NOT_FOUND, detailedError, defaultLogger); + throw createSanitizedError(Parse.Error.OBJECT_NOT_FOUND, detailedError); } else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) { const detailedError = 'Permission denied, user needs to be authenticated.'; - throw createSanitizedError(Parse.Error.OBJECT_NOT_FOUND, detailedError, defaultLogger); + throw createSanitizedError(Parse.Error.OBJECT_NOT_FOUND, detailedError); } // requiresAuthentication passed, just move forward // probably would be wise at some point to rename to 'authenticatedUser' @@ -1424,7 +1424,7 @@ export default class SchemaController { // Reject create when write lockdown if (permissionField == 'writeUserFields' && operation == 'create') { const detailedError = `Permission denied for action ${operation} on class ${className}.`; - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, defaultLogger); + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); } // Process the readUserFields later @@ -1445,7 +1445,7 @@ export default class SchemaController { } const detailedError = `Permission denied for action ${operation} on class ${className}.`; - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, defaultLogger); + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); } // Validates an operation passes class-level-permissions set in the schema diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index 22dcc090c6..4020eb112b 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -7,7 +7,6 @@ import { transformToParse, transformToGraphQL } from '../transformers/schemaFiel import { enforceMasterKeyAccess } from '../parseGraphQLUtils'; import { getClass } from './schemaQueries'; import { createSanitizedError } from '../../SecurityError'; -import defaultLogger from '../../logger'; const load = parseGraphQLSchema => { const createClassMutation = mutationWithClientMutationId({ @@ -35,11 +34,9 @@ const load = parseGraphQLSchema => { enforceMasterKeyAccess(auth, config); if (auth.isReadOnly) { - const loggerOrConfig = config || defaultLogger; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to create a schema.", - loggerOrConfig ); } @@ -137,11 +134,9 @@ const load = parseGraphQLSchema => { enforceMasterKeyAccess(auth, config); if (auth.isReadOnly) { - const loggerOrConfig = config || defaultLogger; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to delete a schema.", - loggerOrConfig ); } diff --git a/src/GraphQL/parseGraphQLUtils.js b/src/GraphQL/parseGraphQLUtils.js index 7c272341cf..d6e1f8c03b 100644 --- a/src/GraphQL/parseGraphQLUtils.js +++ b/src/GraphQL/parseGraphQLUtils.js @@ -1,15 +1,12 @@ import Parse from 'parse/node'; import { GraphQLError } from 'graphql'; import { createSanitizedError } from '../SecurityError'; -import defaultLogger from '../logger'; -export function enforceMasterKeyAccess(auth, config = null) { +export function enforceMasterKeyAccess(auth) { if (!auth.isMaster) { - const loggerOrConfig = config || defaultLogger; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, 'unauthorized: master key is required', - loggerOrConfig ); } } diff --git a/src/RestQuery.js b/src/RestQuery.js index d220dd9727..c3eebcc1ac 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -123,11 +123,9 @@ function _UnsafeRestQuery( if (this.className == '_Session') { if (!this.auth.user) { const detailedError = 'Invalid session token'; - const log = (this.config && this.config.loggerController) || defaultLogger; throw createSanitizedError( Parse.Error.INVALID_SESSION_TOKEN, detailedError, - log ); } this.restWhere = { @@ -809,11 +807,9 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () { for (const key of protectedFields) { if (this.restWhere[key]) { const detailedError = `This user is not allowed to query ${key} on class ${this.className}`; - const log = (this.config && this.config.loggerController) || defaultLogger; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, detailedError, - log ); } } diff --git a/src/RestWrite.js b/src/RestWrite.js index 987f455263..0fb3450c30 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1455,11 +1455,9 @@ RestWrite.prototype.runDatabaseOperation = function () { if (this.className === '_User' && this.query && this.auth.isUnauthenticated()) { const detailedError = `Cannot modify user ${this.query.objectId}.`; - const log = (this.config && this.config.loggerController) || defaultLogger; throw createSanitizedError( Parse.Error.SESSION_MISSING, detailedError, - log ); } diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 9797e8837e..8855c539ac 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -114,8 +114,7 @@ export class ClassesRouter extends PromiseRouter { req.body.objectId.startsWith('role:') ) { const detailedError = 'Invalid object ID.'; - const log = (req.config && req.config.loggerController) || defaultLogger; - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, log); + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); } return rest.create( req.config, diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index c0bd565149..cf281544c8 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -6,7 +6,6 @@ import logger from '../logger'; const triggers = require('../triggers'); const Utils = require('../Utils'); import { createSanitizedError } from '../SecurityError'; -import defaultLogger from '../logger'; export class FilesRouter { expressRouter({ maxUploadSize = '20Mb' } = {}) { @@ -46,9 +45,7 @@ export class FilesRouter { if (!config) { res.status(403); const detailedError = 'Invalid application ID.'; - const log = defaultLogger; - log.error('Security error:', detailedError); - const err = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, log); + const err = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); res.json({ code: err.code, error: err.message }); return; } diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index ce8b09bbed..80a6527a1e 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -4,7 +4,6 @@ import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import * as triggers from '../triggers'; import { createSanitizedError } from '../SecurityError'; -import defaultLogger from '../logger'; const getConfigFromParams = params => { const config = new Parse.Config(); @@ -43,11 +42,9 @@ export class GlobalConfigRouter extends PromiseRouter { async updateGlobalConfig(req) { if (req.auth.isReadOnly) { - const log = (req.config && req.config.loggerController) || defaultLogger; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to update the config.", - log ); } const params = req.body.params || {}; diff --git a/src/Routers/GraphQLRouter.js b/src/Routers/GraphQLRouter.js index 1571bae9ab..ec69eb5aec 100644 --- a/src/Routers/GraphQLRouter.js +++ b/src/Routers/GraphQLRouter.js @@ -2,7 +2,6 @@ import Parse from 'parse/node'; import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import { createSanitizedError } from '../SecurityError'; -import defaultLogger from '../logger'; const GraphQLConfigPath = '/graphql-config'; @@ -16,11 +15,9 @@ export class GraphQLRouter extends PromiseRouter { async updateGraphQLConfig(req) { if (req.auth.isReadOnly) { - const log = (req.config && req.config.loggerController) || defaultLogger; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to update the GraphQL config.", - log ); } const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {}); diff --git a/src/Routers/PurgeRouter.js b/src/Routers/PurgeRouter.js index 980cde54a5..55fac69d79 100644 --- a/src/Routers/PurgeRouter.js +++ b/src/Routers/PurgeRouter.js @@ -2,16 +2,13 @@ import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import Parse from 'parse/node'; import { createSanitizedError } from '../SecurityError'; -import defaultLogger from '../logger'; export class PurgeRouter extends PromiseRouter { handlePurge(req) { if (req.auth.isReadOnly) { - const log = (req.config && req.config.loggerController) || defaultLogger; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to purge a schema.", - log ); } return req.config.database diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js index fab3807021..2cfa0e9690 100644 --- a/src/Routers/PushRouter.js +++ b/src/Routers/PushRouter.js @@ -2,7 +2,6 @@ import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import { Parse } from 'parse/node'; import { createSanitizedError } from '../SecurityError'; -import defaultLogger from '../logger'; export class PushRouter extends PromiseRouter { mountRoutes() { @@ -11,11 +10,9 @@ export class PushRouter extends PromiseRouter { static handlePOST(req) { if (req.auth.isReadOnly) { - const log = (req.config && req.config.loggerController) || defaultLogger; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to send push notifications.", - log ); } const pushController = req.config.pushController; diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index 2fdac66f95..8541c30ec2 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -6,7 +6,6 @@ var Parse = require('parse/node').Parse, import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import { createSanitizedError } from '../SecurityError'; -import defaultLogger from '../logger'; function classNameMismatchResponse(bodyClass, pathClass) { throw new Parse.Error( @@ -74,11 +73,9 @@ export const internalUpdateSchema = async (className, body, config) => { async function createSchema(req) { checkIfDefinedSchemasIsUsed(req); if (req.auth.isReadOnly) { - const log = (req.config && req.config.loggerController) || defaultLogger; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to create a schema.", - log ); } if (req.params.className && req.body?.className) { @@ -98,11 +95,9 @@ async function createSchema(req) { function modifySchema(req) { checkIfDefinedSchemasIsUsed(req); if (req.auth.isReadOnly) { - const log = (req.config && req.config.loggerController) || defaultLogger; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to update a schema.", - log ); } if (req.body?.className && req.body.className != req.params.className) { @@ -115,11 +110,9 @@ function modifySchema(req) { const deleteSchema = req => { if (req.auth.isReadOnly) { - const log = (req.config && req.config.loggerController) || defaultLogger; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to delete a schema.", - log ); } if (!SchemaController.classNameIsValid(req.params.className)) { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 19c7c4bd9e..0ee041161e 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -17,7 +17,6 @@ import { promiseEnsureIdempotency } from '../middlewares'; import RestWrite from '../RestWrite'; import { logger } from '../logger'; import { createSanitizedError } from '../SecurityError'; -import defaultLogger from '../logger'; export class UsersRouter extends ClassesRouter { className() { @@ -335,11 +334,9 @@ export class UsersRouter extends ClassesRouter { */ async handleLogInAs(req) { if (!req.auth.isMaster) { - const log = (req.config && req.config.loggerController) || defaultLogger; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, 'master key is required', - log ); } diff --git a/src/SecurityError.js b/src/SecurityError.js index df7cf5bdae..18d48a6ac3 100644 --- a/src/SecurityError.js +++ b/src/SecurityError.js @@ -9,19 +9,9 @@ import defaultLogger from './logger'; * @param {Object} loggerOrConfig - Optional logger instance or config object (from req.config.loggerController or default) * @returns {Parse.Error} A Parse.Error with sanitized message */ -export function createSanitizedError(errorCode, detailedMessage, loggerOrConfig = null) { - let log = defaultLogger; - if (loggerOrConfig) { - if (loggerOrConfig.loggerController) { - log = loggerOrConfig.loggerController; - } else if (loggerOrConfig.error) { - // It's a logger instance - log = loggerOrConfig; - } - } - +export function createSanitizedError(errorCode, detailedMessage) { // Keep log on server side - log.error('Security error:', detailedMessage); + defaultLogger.error('Security error:', detailedMessage); return new Parse.Error(errorCode, 'Permission denied'); } @@ -35,18 +25,8 @@ export function createSanitizedError(errorCode, detailedMessage, loggerOrConfig * @param {Object} loggerOrConfig - Optional logger instance or config object * @returns {Error} An Error with sanitized message */ -export function createSanitizedHttpError(statusCode, detailedMessage, loggerOrConfig = null) { - let log = defaultLogger; - if (loggerOrConfig) { - if (loggerOrConfig.loggerController) { - log = loggerOrConfig.loggerController; - } else if (loggerOrConfig.error) { - log = loggerOrConfig; - } - } - - // Keep log on server side - log.error('Security error:', detailedMessage); +export function createSanitizedHttpError(statusCode, detailedMessage) { + defaultLogger.error('Security error:', detailedMessage); const error = new Error(); error.status = statusCode; diff --git a/src/SharedRest.js b/src/SharedRest.js index cc833d7d8c..1ccfb52412 100644 --- a/src/SharedRest.js +++ b/src/SharedRest.js @@ -7,15 +7,13 @@ const classesWithMasterOnlyAccess = [ '_Idempotency', ]; const { createSanitizedError } = require('./SecurityError'); -const defaultLogger = require('./logger').default; // Disallowing access to the _Role collection except by master key -function enforceRoleSecurity(method, className, auth, config = null) { +function enforceRoleSecurity(method, className, auth) { if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) { if (method === 'delete' || method === 'find') { const detailedError = `Clients aren't allowed to perform the ${method} operation on the installation collection.`; - const loggerOrConfig = config || defaultLogger; - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, loggerOrConfig); + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); } } @@ -26,15 +24,13 @@ function enforceRoleSecurity(method, className, auth, config = null) { !auth.isMaintenance ) { const detailedError = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`; - const loggerOrConfig = config || defaultLogger; - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, loggerOrConfig); + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); } // readOnly masterKey is not allowed if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) { const detailedError = `read-only masterKey isn't allowed to perform the ${method} operation.`; - const loggerOrConfig = config || defaultLogger; - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError, loggerOrConfig); + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); } } diff --git a/src/middlewares.js b/src/middlewares.js index 6e135f047f..8516cbf887 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -502,8 +502,7 @@ export function handleParseErrors(err, req, res, next) { export function enforceMasterKeyAccess(req, res, next) { if (!req.auth.isMaster) { - const log = (req.config && req.config.loggerController) || defaultLogger; - const error = createSanitizedHttpError(403, 'unauthorized: master key is required', log); + const error = createSanitizedHttpError(403, 'unauthorized: master key is required'); res.status(error.status); res.end(`{"error":"${error.message}"}`); return; @@ -513,8 +512,7 @@ export function enforceMasterKeyAccess(req, res, next) { export function promiseEnforceMasterKeyAccess(request) { if (!request.auth.isMaster) { - const log = (request.config && request.config.loggerController) || defaultLogger; - throw createSanitizedHttpError(403, 'unauthorized: master key is required', log); + throw createSanitizedHttpError(403, 'unauthorized: master key is required'); } return Promise.resolve(); } From bb3eef0055e720b7e771befca29a2adb55baf6f2 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:40:41 +0100 Subject: [PATCH 05/13] fix: update --- src/GraphQL/loaders/schemaMutations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index 4020eb112b..db25df89a5 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -83,7 +83,7 @@ const load = parseGraphQLSchema => { enforceMasterKeyAccess(auth, config); if (auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to update a schema." ); From 79ed425059b3698c81dd60664acc27ad1f456f81 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Wed, 19 Nov 2025 09:08:03 +0100 Subject: [PATCH 06/13] fix: remove useless config --- src/GraphQL/loaders/schemaMutations.js | 6 +++--- src/GraphQL/loaders/schemaQueries.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index db25df89a5..31f2fa9739 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -31,7 +31,7 @@ const load = parseGraphQLSchema => { const { name, schemaFields } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth, config); + enforceMasterKeyAccess(auth); if (auth.isReadOnly) { throw createSanitizedError( @@ -80,7 +80,7 @@ const load = parseGraphQLSchema => { const { name, schemaFields } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth, config); + enforceMasterKeyAccess(auth); if (auth.isReadOnly) { throw createSanitizedError( @@ -131,7 +131,7 @@ const load = parseGraphQLSchema => { const { name } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth, config); + enforceMasterKeyAccess(auth); if (auth.isReadOnly) { throw createSanitizedError( diff --git a/src/GraphQL/loaders/schemaQueries.js b/src/GraphQL/loaders/schemaQueries.js index 2956b47934..25bc071919 100644 --- a/src/GraphQL/loaders/schemaQueries.js +++ b/src/GraphQL/loaders/schemaQueries.js @@ -31,7 +31,7 @@ const load = parseGraphQLSchema => { const { name } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth, config); + enforceMasterKeyAccess(auth); const schema = await config.database.loadSchema({ clearCache: true }); const parseClass = await getClass(name, schema); @@ -57,7 +57,7 @@ const load = parseGraphQLSchema => { try { const { config, auth } = context; - enforceMasterKeyAccess(auth, config); + enforceMasterKeyAccess(auth); const schema = await config.database.loadSchema({ clearCache: true }); return (await schema.getAllClasses(true)).map(parseClass => ({ From fdf074a786f51ea5651e6dab47284515cd64b58c Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:01:00 +0100 Subject: [PATCH 07/13] fix: feedbacks --- spec/AudienceRouter.spec.js | 21 +++++++++++++ spec/LogsRouter.spec.js | 5 ++++ spec/ParseAPI.spec.js | 10 +++++-- spec/ParseFile.spec.js | 12 ++++++++ spec/ParseGlobalConfig.spec.js | 5 ++++ spec/ParseGraphQLServer.spec.js | 24 +++++++++++++++ spec/ParseQuery.Aggregate.spec.js | 5 ++++ spec/ParseUser.spec.js | 16 ++++++++++ spec/RestQuery.spec.js | 6 +++- spec/Schema.spec.js | 26 ++++++++++++++++ spec/features.spec.js | 5 ++++ spec/rest.spec.js | 5 ++++ spec/schemas.spec.js | 41 ++++++++++++++++++++++++++ src/Controllers/SchemaController.js | 26 ++++++++++------ src/{SecurityError.js => Error.js} | 23 ++++++++++----- src/GraphQL/loaders/schemaMutations.js | 2 +- src/GraphQL/parseGraphQLUtils.js | 2 +- src/RestQuery.js | 11 ++----- src/RestWrite.js | 14 ++++----- src/Routers/ClassesRouter.js | 5 ++-- src/Routers/FilesRouter.js | 5 ++-- src/Routers/GlobalConfigRouter.js | 2 +- src/Routers/GraphQLRouter.js | 2 +- src/Routers/PurgeRouter.js | 2 +- src/Routers/PushRouter.js | 2 +- src/Routers/SchemasRouter.js | 2 +- src/Routers/UsersRouter.js | 2 +- src/SharedRest.js | 20 ++++++++----- src/TestUtils.js | 21 +++++++++++++ src/middlewares.js | 2 +- src/rest.js | 5 ++-- 31 files changed, 268 insertions(+), 61 deletions(-) rename src/{SecurityError.js => Error.js} (54%) diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js index 472cd33990..c191adf57a 100644 --- a/spec/AudienceRouter.spec.js +++ b/spec/AudienceRouter.spec.js @@ -2,6 +2,7 @@ const auth = require('../lib/Auth'); const Config = require('../lib/Config'); const rest = require('../lib/rest'); const request = require('../lib/request'); +const { getSanitizedErrorCall } = require('../lib/TestUtils'); const AudiencesRouter = require('../lib/Routers/AudiencesRouter').AudiencesRouter; describe('AudiencesRouter', () => { @@ -263,6 +264,9 @@ describe('AudiencesRouter', () => { }); it('should only create with master key', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); Parse._request('POST', 'push_audiences', { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' }), @@ -270,48 +274,65 @@ describe('AudiencesRouter', () => { () => {}, error => { expect(error.message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); done(); } ); }); it('should only find with master key', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); Parse._request('GET', 'push_audiences', {}).then( () => {}, error => { expect(error.message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); done(); } ); }); it('should only get with master key', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); Parse._request('GET', `push_audiences/someId`, {}).then( () => {}, error => { expect(error.message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); done(); } ); }); it('should only update with master key', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); Parse._request('PUT', `push_audiences/someId`, { name: 'My Audience 2', }).then( () => {}, error => { expect(error.message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); done(); } ); }); it('should only delete with master key', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); Parse._request('DELETE', `push_audiences/someId`, {}).then( () => {}, error => { expect(error.message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); done(); } ); diff --git a/spec/LogsRouter.spec.js b/spec/LogsRouter.spec.js index e2e91002ba..502e005676 100644 --- a/spec/LogsRouter.spec.js +++ b/spec/LogsRouter.spec.js @@ -1,6 +1,7 @@ 'use strict'; const request = require('../lib/request'); +const { getSanitizedErrorCall } = require('../lib/TestUtils'); const LogsRouter = require('../lib/Routers/LogsRouter').LogsRouter; const LoggerController = require('../lib/Controllers/LoggerController').LoggerController; const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter') @@ -52,6 +53,9 @@ describe_only(() => { }); it('can check invalid master key of request', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); request({ url: 'http://localhost:8378/1/scriptlog', headers: { @@ -62,6 +66,7 @@ describe_only(() => { const body = response.data; expect(response.status).toEqual(403); expect(body.error).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); done(); }); }); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 9412353684..b7282647e7 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -6,7 +6,7 @@ const request = require('../lib/request'); const Parse = require('parse/node'); const Config = require('../lib/Config'); const SchemaController = require('../lib/Controllers/SchemaController'); -const TestUtils = require('../lib/TestUtils'); +const { getSanitizedErrorCall, destroyAllDataPermanently } = require('../lib/TestUtils'); const userSchema = SchemaController.convertSchemaToAdapterSchema({ className: '_User', @@ -169,7 +169,7 @@ describe('miscellaneous', () => { } const config = Config.get('test'); // Remove existing data to clear out unique index - TestUtils.destroyAllDataPermanently() + destroyAllDataPermanently() .then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] })) .then(() => config.database.adapter.createClass('_User', userSchema)) .then(() => @@ -210,7 +210,7 @@ describe('miscellaneous', () => { it_id('d00f907e-41b9-40f6-8168-63e832199a8c')(it)('ensure that if people already have duplicate emails, they can still sign up new users', done => { const config = Config.get('test'); // Remove existing data to clear out unique index - TestUtils.destroyAllDataPermanently() + destroyAllDataPermanently() .then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] })) .then(() => config.database.adapter.createClass('_User', userSchema)) .then(() => @@ -1710,11 +1710,14 @@ describe('miscellaneous', () => { }); it('fail on purge all objects in class without master key', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', }; + const callCountBefore = sanitizedErrorCall.callCountBefore(); request({ method: 'DELETE', headers: headers, @@ -1725,6 +1728,7 @@ describe('miscellaneous', () => { }) .catch(response => { expect(response.data.error).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); done(); }); }); diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 9a8eb7840e..8e9bf7d9c2 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -5,6 +5,7 @@ const { FilesController } = require('../lib/Controllers/FilesController'); const request = require('../lib/request'); +const { getSanitizedErrorCall } = require('../lib/TestUtils'); const str = 'Hello World!'; const data = []; @@ -132,6 +133,8 @@ describe('Parse.File testing', () => { }); it('blocks file deletions with missing or incorrect master-key header', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const headers = { 'Content-Type': 'image/jpeg', 'X-Parse-Application-Id': 'test', @@ -146,6 +149,7 @@ describe('Parse.File testing', () => { const b = response.data; expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/); // missing X-Parse-Master-Key header + const callCountBefore = sanitizedErrorCall.callCountBefore(); request({ method: 'DELETE', headers: { @@ -157,7 +161,9 @@ describe('Parse.File testing', () => { const del_b = response.data; expect(response.status).toEqual(403); expect(del_b.error).toBe('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); // incorrect X-Parse-Master-Key header + const callCountBefore2 = sanitizedErrorCall.callCountBefore(); request({ method: 'DELETE', headers: { @@ -170,6 +176,7 @@ describe('Parse.File testing', () => { const del_b2 = response.data; expect(response.status).toEqual(403); expect(del_b2.error).toBe('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore2); done(); }); }); @@ -756,11 +763,16 @@ describe('Parse.File testing', () => { describe('getting files', () => { it('does not crash on file request with invalid app ID', async () => { + const { getSanitizedErrorCall } = require('../lib/TestUtils'); + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); const res1 = await request({ url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt', }).catch(e => e); expect(res1.status).toBe(403); expect(res1.data).toEqual({ code: 119, error: 'Permission denied' }); + sanitizedErrorCall.checkMessage('Invalid application ID.', callCountBefore); // Ensure server did not crash const res2 = await request({ url: 'http://localhost:8378/1/health' }); expect(res2.status).toEqual(200); diff --git a/spec/ParseGlobalConfig.spec.js b/spec/ParseGlobalConfig.spec.js index e85d252380..9ae5130d54 100644 --- a/spec/ParseGlobalConfig.spec.js +++ b/spec/ParseGlobalConfig.spec.js @@ -2,6 +2,7 @@ const request = require('../lib/request'); const Config = require('../lib/Config'); +const { getSanitizedErrorCall } = require('../lib/TestUtils'); describe('a GlobalConfig', () => { beforeEach(async () => { @@ -220,6 +221,9 @@ describe('a GlobalConfig', () => { }); it('fail to update if master key is missing', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); request({ method: 'PUT', url: 'http://localhost:8378/1/config', @@ -234,6 +238,7 @@ describe('a GlobalConfig', () => { const body = response.data; expect(response.status).toEqual(403); expect(body.error).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); done(); }); }); diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 6689e09905..22c8b50a16 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -35,6 +35,7 @@ const { ParseServer } = require('../'); const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer'); const { ReadPreference, Collection } = require('mongodb'); const { v4: uuidv4 } = require('uuid'); +const { getSanitizedErrorCall } = require('../lib/TestUtils'); function handleError(e) { if (e && e.networkError && e.networkError.result && e.networkError.result.errors) { @@ -3488,6 +3489,9 @@ describe('ParseGraphQLServer', () => { }); it('should require master key to create a new class', async () => { + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); try { await apolloClient.mutate({ mutation: gql` @@ -3502,6 +3506,7 @@ describe('ParseGraphQLServer', () => { } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(e.graphQLErrors[0].message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); } }); @@ -3858,6 +3863,9 @@ describe('ParseGraphQLServer', () => { handleError(e); } + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); try { await apolloClient.mutate({ mutation: gql` @@ -3872,6 +3880,7 @@ describe('ParseGraphQLServer', () => { } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(e.graphQLErrors[0].message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); } }); @@ -4083,6 +4092,10 @@ describe('ParseGraphQLServer', () => { handleError(e); } + const { getSanitizedErrorCall } = require('../lib/TestUtils'); + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); try { await apolloClient.mutate({ mutation: gql` @@ -4097,6 +4110,7 @@ describe('ParseGraphQLServer', () => { } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(e.graphQLErrors[0].message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); } }); @@ -4124,6 +4138,10 @@ describe('ParseGraphQLServer', () => { }); it('should require master key to get an existing class', async () => { + const { getSanitizedErrorCall } = require('../lib/TestUtils'); + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); try { await apolloClient.query({ query: gql` @@ -4138,10 +4156,15 @@ describe('ParseGraphQLServer', () => { } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(e.graphQLErrors[0].message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); } }); it('should require master key to find the existing classes', async () => { + const { getSanitizedErrorCall } = require('../lib/TestUtils'); + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); try { await apolloClient.query({ query: gql` @@ -4156,6 +4179,7 @@ describe('ParseGraphQLServer', () => { } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(e.graphQLErrors[0].message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); } }); }); diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index 07a2056b78..0f431c31c7 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -2,6 +2,7 @@ const Parse = require('parse/node'); const request = require('../lib/request'); const Config = require('../lib/Config'); +const { getSanitizedErrorCall } = require('../lib/TestUtils'); const masterKeyHeaders = { 'X-Parse-Application-Id': 'test', @@ -74,10 +75,14 @@ describe('Parse.Query Aggregate testing', () => { }); it('should only query aggregate with master key', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); Parse._request('GET', `aggregate/someClass`, {}).then( () => {}, error => { expect(error.message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); done(); } ); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 133d18ebae..702c3851de 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -12,6 +12,7 @@ const request = require('../lib/request'); const passwordCrypto = require('../lib/password'); const Config = require('../lib/Config'); const cryptoUtils = require('../lib/cryptoUtils'); +const { getSanitizedErrorCall } = require('../lib/TestUtils'); describe('allowExpiredAuthDataToken option', () => { it('should accept true value', async () => { @@ -2632,6 +2633,8 @@ describe('Parse.User testing', () => { }); it('cannot delete session if no sessionToken', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + let callCountBefore = 0; Promise.resolve() .then(() => { return Parse.User.signUp('test1', 'test', { foo: 'bar' }); @@ -2651,6 +2654,7 @@ describe('Parse.User testing', () => { const b = response.data; expect(b.results.length).toEqual(1); const objId = b.results[0].objectId; + callCountBefore = sanitizedErrorCall.callCountBefore(); request({ method: 'DELETE', headers: { @@ -2662,6 +2666,7 @@ describe('Parse.User testing', () => { const b = response.data; expect(b.code).toEqual(209); expect(b.error).toBe('Permission denied'); + sanitizedErrorCall.checkMessage('Invalid session token', callCountBefore); done(); }); }); @@ -3355,6 +3360,9 @@ describe('Parse.User testing', () => { sendMail: () => Promise.resolve(), }; + let sanitizedErrorCall; + let callCountBefore = 0; + const user = new Parse.User(); user.set({ username: 'hello', @@ -3369,9 +3377,11 @@ describe('Parse.User testing', () => { publicServerURL: 'http://localhost:8378/1', }) .then(() => { + sanitizedErrorCall = getSanitizedErrorCall(); return user.signUp(); }) .then(() => { + callCountBefore = sanitizedErrorCall.callCountBefore(); return Parse.User.current().set('emailVerified', true).save(); }) .then(() => { @@ -3380,6 +3390,8 @@ describe('Parse.User testing', () => { }) .catch(err => { expect(err.message).toBe('Permission denied'); + sanitizedErrorCall.checkMessage("Clients aren't allowed to manually update email verification.", callCountBefore); + done(); }); }); @@ -4372,10 +4384,13 @@ describe('login as other user', () => { }); it('rejects creating a session for another user without the master key', async done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + await Parse.User.signUp('some_user', 'some_password'); const userId = Parse.User.current().id; await Parse.User.logOut(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); try { await request({ method: 'POST', @@ -4394,6 +4409,7 @@ describe('login as other user', () => { } catch (err) { expect(err.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(err.data.error).toBe('Permission denied'); + sanitizedErrorCall.checkMessage('master key is required', callCountBefore); } const sessionsQuery = new Parse.Query(Parse.Session); diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index e88236dbeb..880ef79f5d 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -5,7 +5,7 @@ const Config = require('../lib/Config'); const rest = require('../lib/rest'); const RestQuery = require('../lib/RestQuery'); const request = require('../lib/request'); - +const { getSanitizedErrorCall } = require('../lib/TestUtils'); const querystring = require('querystring'); let config; @@ -155,9 +155,12 @@ describe('rest query', () => { }); it('query non-existent class when disabled client class creation', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const customConfig = Object.assign({}, config, { allowClientClassCreation: false, }); + const callCountBefore = sanitizedErrorCall.callCountBefore(); rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}).then( () => { fail('Should throw an error'); @@ -166,6 +169,7 @@ describe('rest query', () => { err => { expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(err.message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('This user is not allowed to access ' + 'non-existent class: ClientClassCreation', callCountBefore); done(); } ); diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 8fa62556fa..fcf571f67b 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -2,6 +2,7 @@ const Config = require('../lib/Config'); const SchemaController = require('../lib/Controllers/SchemaController'); +const { getSanitizedErrorCall } = require('../lib/TestUtils'); const dd = require('deep-diff'); let config; @@ -249,6 +250,9 @@ describe('SchemaController', () => { }); it('class-level permissions test count', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + let callCountBefore = 0; + let obj; return ( config.database @@ -275,6 +279,7 @@ describe('SchemaController', () => { }) .then(results => { expect(results.length).toBe(1); + callCountBefore = sanitizedErrorCall.callCountBefore(); const query = new Parse.Query('Stuff'); return query.count(); }) @@ -284,6 +289,8 @@ describe('SchemaController', () => { }, err => { expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + sanitizedErrorCall.checkMessage('Permission denied for action count on class Stuff', callCountBefore); done(); } ) @@ -1439,6 +1446,9 @@ describe('Class Level Permissions for requiredAuth', () => { } it('required auth test find', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + let callCountBefore = 0; + config.database .loadSchema() .then(schema => { @@ -1453,6 +1463,7 @@ describe('Class Level Permissions for requiredAuth', () => { }); }) .then(() => { + callCountBefore = sanitizedErrorCall.callCountBefore(); const query = new Parse.Query('Stuff'); return query.find(); }) @@ -1463,6 +1474,7 @@ describe('Class Level Permissions for requiredAuth', () => { }, e => { expect(e.message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('Permission denied, user needs to be authenticated.', callCountBefore); done(); } ); @@ -1537,6 +1549,8 @@ describe('Class Level Permissions for requiredAuth', () => { }); it('required auth should reject create when not authenticated', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + let callCountBefore = 0; config.database .loadSchema() .then(schema => { @@ -1551,6 +1565,7 @@ describe('Class Level Permissions for requiredAuth', () => { }); }) .then(() => { + callCountBefore = sanitizedErrorCall.callCountBefore(); const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff.save(); @@ -1562,6 +1577,7 @@ describe('Class Level Permissions for requiredAuth', () => { }, e => { expect(e.message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('Permission denied, user needs to be authenticated.', callCountBefore); done(); } ); @@ -1619,6 +1635,9 @@ describe('Class Level Permissions for requiredAuth', () => { }); it('required auth test get not authenticated', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + let callCountBefore = 0; + config.database .loadSchema() .then(schema => { @@ -1639,6 +1658,7 @@ describe('Class Level Permissions for requiredAuth', () => { const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff.save().then(() => { + callCountBefore = sanitizedErrorCall.callCountBefore(); const query = new Parse.Query('Stuff'); return query.get(stuff.id); }); @@ -1650,12 +1670,16 @@ describe('Class Level Permissions for requiredAuth', () => { }, e => { expect(e.message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('Permission denied, user needs to be authenticated.', callCountBefore); done(); } ); }); it('required auth test find not authenticated', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + let callCountBefore = 0; + config.database .loadSchema() .then(schema => { @@ -1685,6 +1709,7 @@ describe('Class Level Permissions for requiredAuth', () => { }) .then(result => { expect(result.get('foo')).toEqual('bar'); + callCountBefore = sanitizedErrorCall.callCountBefore(); const query = new Parse.Query('Stuff'); return query.find(); }) @@ -1695,6 +1720,7 @@ describe('Class Level Permissions for requiredAuth', () => { }, e => { expect(e.message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('Permission denied, user needs to be authenticated.', callCountBefore); done(); } ); diff --git a/spec/features.spec.js b/spec/features.spec.js index 84c28fa999..20ee1789a1 100644 --- a/spec/features.spec.js +++ b/spec/features.spec.js @@ -1,6 +1,7 @@ 'use strict'; const request = require('../lib/request'); +const { getSanitizedErrorCall } = require('../lib/TestUtils'); describe('features', () => { it('should return the serverInfo', async () => { @@ -20,6 +21,9 @@ describe('features', () => { }); it('requires the master key to get features', async done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + + const callCountBefore = sanitizedErrorCall.callCountBefore(); try { await request({ url: 'http://localhost:8378/1/serverInfo', @@ -33,6 +37,7 @@ describe('features', () => { } catch (error) { expect(error.status).toEqual(403); expect(error.data.error).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); done(); } }); diff --git a/spec/rest.spec.js b/spec/rest.spec.js index 1fc64945db..9b42739257 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -6,6 +6,7 @@ const Parse = require('parse/node').Parse; const rest = require('../lib/rest'); const RestWrite = require('../lib/RestWrite'); const request = require('../lib/request'); +const { getSanitizedErrorCall } = require('../lib/TestUtils'); let config; let database; @@ -314,9 +315,12 @@ describe('rest create', () => { }); it('handles create on non-existent class when disabled client class creation', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const customConfig = Object.assign({}, config, { allowClientClassCreation: false, }); + const callCountBefore = sanitizedErrorCall.callCountBefore(); rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}).then( () => { fail('Should throw an error'); @@ -325,6 +329,7 @@ describe('rest create', () => { err => { expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(err.message).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage('This user is not allowed to access ' + 'non-existent class: ClientClassCreation', callCountBefore); done(); } ); diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index c72614a314..83f59cf83f 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -5,6 +5,7 @@ const dd = require('deep-diff'); const Config = require('../lib/Config'); const request = require('../lib/request'); const TestUtils = require('../lib/TestUtils'); +const { getSanitizedErrorCall } = require('../lib/TestUtils'); const SchemaController = require('../lib/Controllers/SchemaController').SchemaController; let config; @@ -1807,6 +1808,8 @@ describe('schemas', () => { }); it('should not be able to add a field', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + request({ method: 'POST', url: 'http://localhost:8378/1/schemas/AClass', @@ -1826,6 +1829,7 @@ describe('schemas', () => { }, }, }).then(() => { + const callCountBefore = sanitizedErrorCall.callCountBefore(); const object = new Parse.Object('AClass'); object.set('hello', 'world'); return object.save().then( @@ -1835,6 +1839,8 @@ describe('schemas', () => { }, err => { expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + sanitizedErrorCall.checkMessage('Permission denied for action addField on class AClass', callCountBefore); done(); } ); @@ -2167,6 +2173,8 @@ describe('schemas', () => { } it('validate CLP 1', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); @@ -2198,6 +2206,7 @@ describe('schemas', () => { }); }) .then(() => { + const callCountBefore = sanitizedErrorCall.callCountBefore(); const query = new Parse.Query('AClass'); return query.find().then( () => { @@ -2205,6 +2214,8 @@ describe('schemas', () => { }, err => { expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + sanitizedErrorCall.checkMessage('Permission denied for action find on class AClass', callCountBefore); return Promise.resolve(); } ); @@ -2227,6 +2238,9 @@ describe('schemas', () => { }); it('validate CLP 2', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + let callCountBefore = 0; + const user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); @@ -2258,6 +2272,7 @@ describe('schemas', () => { }); }) .then(() => { + callCountBefore = sanitizedErrorCall.callCountBefore(); const query = new Parse.Query('AClass'); return query.find().then( () => { @@ -2265,6 +2280,8 @@ describe('schemas', () => { }, err => { expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + sanitizedErrorCall.checkMessage('Permission denied for action find on class AClass', callCountBefore); return Promise.resolve(); } ); @@ -2312,6 +2329,9 @@ describe('schemas', () => { }); it('validate CLP 3', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + let callCountBefore = 0; + const user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); @@ -2343,6 +2363,7 @@ describe('schemas', () => { }); }) .then(() => { + callCountBefore = sanitizedErrorCall.callCountBefore(); const query = new Parse.Query('AClass'); return query.find().then( () => { @@ -2350,6 +2371,8 @@ describe('schemas', () => { }, err => { expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + sanitizedErrorCall.checkMessage('Permission denied for action find on class AClass', callCountBefore); return Promise.resolve(); } ); @@ -2388,6 +2411,9 @@ describe('schemas', () => { }); it('validate CLP 4', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + let callCountBefore = 0; + const user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); @@ -2419,6 +2445,7 @@ describe('schemas', () => { }); }) .then(() => { + callCountBefore = sanitizedErrorCall.callCountBefore(); const query = new Parse.Query('AClass'); return query.find().then( () => { @@ -2426,6 +2453,8 @@ describe('schemas', () => { }, err => { expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + sanitizedErrorCall.checkMessage('Permission denied for action find on class AClass', callCountBefore); return Promise.resolve(); } ); @@ -2450,6 +2479,7 @@ describe('schemas', () => { ); }) .then(() => { + callCountBefore = sanitizedErrorCall.callCountBefore(); const query = new Parse.Query('AClass'); return query.find().then( () => { @@ -2457,6 +2487,8 @@ describe('schemas', () => { }, err => { expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + sanitizedErrorCall.checkMessage('Permission denied for action find on class AClass', callCountBefore); return Promise.resolve(); } ); @@ -2479,6 +2511,9 @@ describe('schemas', () => { }); it('validate CLP 5', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + let callCountBefore = 0; + const user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); @@ -2531,6 +2566,7 @@ describe('schemas', () => { return Parse.User.logIn('admin', 'admin'); }) .then(() => { + callCountBefore = sanitizedErrorCall.callCountBefore(); const query = new Parse.Query('AClass'); return query.find(); }) @@ -2541,6 +2577,8 @@ describe('schemas', () => { }, err => { expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + sanitizedErrorCall.checkMessage('Permission denied for action create on class AClass', callCountBefore); return Promise.resolve(); } ) @@ -2548,6 +2586,7 @@ describe('schemas', () => { return Parse.User.logIn('user2', 'user2'); }) .then(() => { + callCountBefore = sanitizedErrorCall.callCountBefore(); const query = new Parse.Query('AClass'); return query.find(); }) @@ -2558,6 +2597,8 @@ describe('schemas', () => { }, err => { expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + sanitizedErrorCall.checkMessage('Permission denied for action find on class AClass', callCountBefore); return Promise.resolve(); } ) diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 6ecea6e9b1..b21d4b3318 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -20,7 +20,7 @@ import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; import SchemaCache from '../Adapters/Cache/SchemaCache'; import DatabaseController from './DatabaseController'; import Config from '../Config'; -import { createSanitizedError } from '../SecurityError'; +import { createSanitizedError } from '../Error'; import defaultLogger from '../logger'; // @flow-disable-next import deepcopy from 'deepcopy'; @@ -1405,11 +1405,15 @@ export default class SchemaController { if (perms['requiresAuthentication']) { // If aclGroup has * (public) if (!aclGroup || aclGroup.length == 0) { - const detailedError = 'Permission denied, user needs to be authenticated.'; - throw createSanitizedError(Parse.Error.OBJECT_NOT_FOUND, detailedError); + throw createSanitizedError( + Parse.Error.OBJECT_NOT_FOUND, + 'Permission denied, user needs to be authenticated.' + ); } else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) { - const detailedError = 'Permission denied, user needs to be authenticated.'; - throw createSanitizedError(Parse.Error.OBJECT_NOT_FOUND, detailedError); + throw createSanitizedError( + Parse.Error.OBJECT_NOT_FOUND, + 'Permission denied, user needs to be authenticated.' + ); } // requiresAuthentication passed, just move forward // probably would be wise at some point to rename to 'authenticatedUser' @@ -1423,8 +1427,10 @@ export default class SchemaController { // Reject create when write lockdown if (permissionField == 'writeUserFields' && operation == 'create') { - const detailedError = `Permission denied for action ${operation} on class ${className}.`; - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + `Permission denied for action ${operation} on class ${className}.` + ); } // Process the readUserFields later @@ -1444,8 +1450,10 @@ export default class SchemaController { } } - const detailedError = `Permission denied for action ${operation} on class ${className}.`; - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + `Permission denied for action ${operation} on class ${className}.` + ); } // Validates an operation passes class-level-permissions set in the schema diff --git a/src/SecurityError.js b/src/Error.js similarity index 54% rename from src/SecurityError.js rename to src/Error.js index 18d48a6ac3..1744f1c303 100644 --- a/src/SecurityError.js +++ b/src/Error.js @@ -1,32 +1,39 @@ import defaultLogger from './logger'; /** - * Creates a sanitized security error that hides detailed information from clients + * Creates a sanitized error that hides detailed information from clients * while logging the detailed message server-side. * * @param {number} errorCode - The Parse.Error code (e.g., Parse.Error.OPERATION_FORBIDDEN) * @param {string} detailedMessage - The detailed error message to log server-side - * @param {Object} loggerOrConfig - Optional logger instance or config object (from req.config.loggerController or default) * @returns {Parse.Error} A Parse.Error with sanitized message */ export function createSanitizedError(errorCode, detailedMessage) { - // Keep log on server side - defaultLogger.error('Security error:', detailedMessage); + // On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file + if (process.env.TESTING) { + defaultLogger.error('Sanitized error:', detailedMessage); + } else { + defaultLogger.error(detailedMessage); + } return new Parse.Error(errorCode, 'Permission denied'); } /** - * Creates a sanitized security error from a regular Error object - * Used for non-Parse.Error security errors (e.g., Express errors) + * Creates a sanitized error from a regular Error object + * Used for non-Parse.Error errors (e.g., Express errors) * * @param {number} statusCode - HTTP status code (e.g., 403) * @param {string} detailedMessage - The detailed error message to log server-side - * @param {Object} loggerOrConfig - Optional logger instance or config object * @returns {Error} An Error with sanitized message */ export function createSanitizedHttpError(statusCode, detailedMessage) { - defaultLogger.error('Security error:', detailedMessage); + // On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file + if (process.env.TESTING) { + defaultLogger.error('Sanitized error:', detailedMessage); + } else { + defaultLogger.error(detailedMessage); + } const error = new Error(); error.status = statusCode; diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index 31f2fa9739..5dd8969bd9 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -6,7 +6,7 @@ import * as schemaTypes from './schemaTypes'; import { transformToParse, transformToGraphQL } from '../transformers/schemaFields'; import { enforceMasterKeyAccess } from '../parseGraphQLUtils'; import { getClass } from './schemaQueries'; -import { createSanitizedError } from '../../SecurityError'; +import { createSanitizedError } from '../../Error'; const load = parseGraphQLSchema => { const createClassMutation = mutationWithClientMutationId({ diff --git a/src/GraphQL/parseGraphQLUtils.js b/src/GraphQL/parseGraphQLUtils.js index d6e1f8c03b..1a0b266a36 100644 --- a/src/GraphQL/parseGraphQLUtils.js +++ b/src/GraphQL/parseGraphQLUtils.js @@ -1,6 +1,6 @@ import Parse from 'parse/node'; import { GraphQLError } from 'graphql'; -import { createSanitizedError } from '../SecurityError'; +import { createSanitizedError } from '../Error'; export function enforceMasterKeyAccess(auth) { if (!auth.isMaster) { diff --git a/src/RestQuery.js b/src/RestQuery.js index c3eebcc1ac..b7dc3e54b2 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -7,7 +7,7 @@ const triggers = require('./triggers'); const { continueWhile } = require('parse/lib/node/promiseUtils'); const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL']; const { enforceRoleSecurity } = require('./SharedRest'); -const { createSanitizedError } = require('./SecurityError'); +const { createSanitizedError } = require('./Error'); const defaultLogger = require('./logger').default; // restOptions can include: @@ -122,11 +122,7 @@ function _UnsafeRestQuery( if (!this.auth.isMaster) { if (this.className == '_Session') { if (!this.auth.user) { - const detailedError = 'Invalid session token'; - throw createSanitizedError( - Parse.Error.INVALID_SESSION_TOKEN, - detailedError, - ); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } this.restWhere = { $and: [ @@ -806,10 +802,9 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () { ) || []; for (const key of protectedFields) { if (this.restWhere[key]) { - const detailedError = `This user is not allowed to query ${key} on class ${this.className}`; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - detailedError, + `This user is not allowed to query ${key} on class ${this.className}` ); } } diff --git a/src/RestWrite.js b/src/RestWrite.js index 0fb3450c30..447af1d450 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -17,7 +17,7 @@ import RestQuery from './RestQuery'; import _ from 'lodash'; import logger from './logger'; import { requiredColumns } from './Controllers/SchemaController'; -import { createSanitizedError } from './SecurityError'; +import { createSanitizedError } from './Error'; import defaultLogger from './logger'; // query and data are both provided in REST API format. So data @@ -201,11 +201,10 @@ RestWrite.prototype.validateClientClassCreation = function () { .then(schemaController => schemaController.hasClass(this.className)) .then(hasClass => { if (hasClass !== true) { - const detailedError = 'This user is not allowed to access non-existent class: ' + this.className; const log = (this.config && this.config.loggerController) || defaultLogger; throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - detailedError, + 'This user is not allowed to access non-existent class: ' + this.className, log ); } @@ -571,7 +570,6 @@ RestWrite.prototype.handleAuthData = async function (authData) { // User found with provided authData if (results.length === 1) { - this.storage.authProvider = Object.keys(authData).join(','); const { hasMutatedAuthData, mutatedAuthData } = Auth.hasMutatedAuthData( @@ -665,7 +663,10 @@ RestWrite.prototype.checkRestrictedFields = async function () { } if (!this.auth.isMaintenance && !this.auth.isMaster && 'emailVerified' in this.data) { - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Clients aren\'t allowed to manually update email verification.'); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + "Clients aren't allowed to manually update email verification." + ); } }; @@ -1454,10 +1455,9 @@ RestWrite.prototype.runDatabaseOperation = function () { } if (this.className === '_User' && this.query && this.auth.isUnauthenticated()) { - const detailedError = `Cannot modify user ${this.query.objectId}.`; throw createSanitizedError( Parse.Error.SESSION_MISSING, - detailedError, + `Cannot modify user ${this.query.objectId}.` ); } diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 8855c539ac..ae774705d6 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -3,7 +3,7 @@ import rest from '../rest'; import _ from 'lodash'; import Parse from 'parse/node'; import { promiseEnsureIdempotency } from '../middlewares'; -import { createSanitizedError } from '../SecurityError'; +import { createSanitizedError } from '../Error'; import defaultLogger from '../logger'; const ALLOWED_GET_QUERY_KEYS = [ @@ -113,8 +113,7 @@ export class ClassesRouter extends PromiseRouter { typeof req.body?.objectId === 'string' && req.body.objectId.startsWith('role:') ) { - const detailedError = 'Invalid object ID.'; - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); + throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.'); } return rest.create( req.config, diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index cf281544c8..12c523c3b0 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -5,7 +5,7 @@ import Config from '../Config'; import logger from '../logger'; const triggers = require('../triggers'); const Utils = require('../Utils'); -import { createSanitizedError } from '../SecurityError'; +import { createSanitizedError } from '../Error'; export class FilesRouter { expressRouter({ maxUploadSize = '20Mb' } = {}) { @@ -44,8 +44,7 @@ export class FilesRouter { const config = Config.get(req.params.appId); if (!config) { res.status(403); - const detailedError = 'Invalid application ID.'; - const err = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); + const err = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid application ID.'); res.json({ code: err.code, error: err.message }); return; } diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index 80a6527a1e..4b107ee878 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -3,7 +3,7 @@ import Parse from 'parse/node'; import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import * as triggers from '../triggers'; -import { createSanitizedError } from '../SecurityError'; +import { createSanitizedError } from '../Error'; const getConfigFromParams = params => { const config = new Parse.Config(); diff --git a/src/Routers/GraphQLRouter.js b/src/Routers/GraphQLRouter.js index ec69eb5aec..d785afbdf2 100644 --- a/src/Routers/GraphQLRouter.js +++ b/src/Routers/GraphQLRouter.js @@ -1,7 +1,7 @@ import Parse from 'parse/node'; import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; -import { createSanitizedError } from '../SecurityError'; +import { createSanitizedError } from '../Error'; const GraphQLConfigPath = '/graphql-config'; diff --git a/src/Routers/PurgeRouter.js b/src/Routers/PurgeRouter.js index 55fac69d79..7b992a48e2 100644 --- a/src/Routers/PurgeRouter.js +++ b/src/Routers/PurgeRouter.js @@ -1,7 +1,7 @@ import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import Parse from 'parse/node'; -import { createSanitizedError } from '../SecurityError'; +import { createSanitizedError } from '../Error'; export class PurgeRouter extends PromiseRouter { handlePurge(req) { diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js index 2cfa0e9690..123677f138 100644 --- a/src/Routers/PushRouter.js +++ b/src/Routers/PushRouter.js @@ -1,7 +1,7 @@ import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import { Parse } from 'parse/node'; -import { createSanitizedError } from '../SecurityError'; +import { createSanitizedError } from '../Error'; export class PushRouter extends PromiseRouter { mountRoutes() { diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index 8541c30ec2..ff55711a69 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -5,7 +5,7 @@ var Parse = require('parse/node').Parse, import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; -import { createSanitizedError } from '../SecurityError'; +import { createSanitizedError } from '../Error'; function classNameMismatchResponse(bodyClass, pathClass) { throw new Parse.Error( diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 0ee041161e..e1db8dd9ec 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -16,7 +16,7 @@ import { import { promiseEnsureIdempotency } from '../middlewares'; import RestWrite from '../RestWrite'; import { logger } from '../logger'; -import { createSanitizedError } from '../SecurityError'; +import { createSanitizedError } from '../Error'; export class UsersRouter extends ClassesRouter { className() { diff --git a/src/SharedRest.js b/src/SharedRest.js index 1ccfb52412..1d342b595a 100644 --- a/src/SharedRest.js +++ b/src/SharedRest.js @@ -6,14 +6,16 @@ const classesWithMasterOnlyAccess = [ '_JobSchedule', '_Idempotency', ]; -const { createSanitizedError } = require('./SecurityError'); +const { createSanitizedError } = require('./Error'); // Disallowing access to the _Role collection except by master key function enforceRoleSecurity(method, className, auth) { if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) { if (method === 'delete' || method === 'find') { - const detailedError = `Clients aren't allowed to perform the ${method} operation on the installation collection.`; - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + `Clients aren't allowed to perform the ${method} operation on the installation collection.` + ); } } @@ -23,14 +25,18 @@ function enforceRoleSecurity(method, className, auth) { !auth.isMaster && !auth.isMaintenance ) { - const detailedError = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`; - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + `Clients aren't allowed to perform the ${method} operation on the ${className} collection.` + ); } // readOnly masterKey is not allowed if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) { - const detailedError = `read-only masterKey isn't allowed to perform the ${method} operation.`; - throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, detailedError); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + `read-only masterKey isn't allowed to perform the ${method} operation.` + ); } } diff --git a/src/TestUtils.js b/src/TestUtils.js index 2cd1493511..33ec2b214a 100644 --- a/src/TestUtils.js +++ b/src/TestUtils.js @@ -81,3 +81,24 @@ export class Connections { return this.sockets.size; } } + +export function getSanitizedErrorCall() { + const logger = require('../lib/logger').default; + // eslint-disable-next-line no-undef + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + + return { + callCountBefore: () => loggerErrorSpy.calls.count(), + checkMessage: (message, callCountBefore) => { + // eslint-disable-next-line no-undef + expect(loggerErrorSpy.calls.count()).toBeGreaterThan(callCountBefore); + const calls = loggerErrorSpy.calls.all(); + const recentCalls = calls.slice(callCountBefore); + const sanitizedErrorCall = recentCalls.find(call => call.args[0] === 'Sanitized error:'); + // eslint-disable-next-line no-undef + expect(sanitizedErrorCall).toBeDefined(); + // eslint-disable-next-line no-undef + expect(sanitizedErrorCall.args[1]).toContain(message); + }, + }; +} diff --git a/src/middlewares.js b/src/middlewares.js index 8516cbf887..c5b644379e 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -13,7 +13,7 @@ import { pathToRegexp } from 'path-to-regexp'; import RedisStore from 'rate-limit-redis'; import { createClient } from 'redis'; import { BlockList, isIPv4 } from 'net'; -import { createSanitizedHttpError } from './SecurityError'; +import { createSanitizedHttpError } from './Error'; export const DEFAULT_ALLOWED_HEADERS = 'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, X-Parse-Request-Id, Content-Type, Pragma, Cache-Control'; diff --git a/src/rest.js b/src/rest.js index 511363d4c3..933f6d7487 100644 --- a/src/rest.js +++ b/src/rest.js @@ -326,10 +326,9 @@ function handleSessionMissingError(error, className, auth) { !auth.isMaster && !auth.isMaintenance ) { - const { createSanitizedError } = require('./SecurityError'); - const detailedError = 'Insufficient auth.'; + const { createSanitizedError } = require('./Error'); - throw createSanitizedError(Parse.Error.SESSION_MISSING, detailedError); + throw createSanitizedError(Parse.Error.SESSION_MISSING, 'Insufficient auth.'); } throw error; } From f40fda601cb0eb917e9e1e3c86fc9d024aa1af2e Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:01:00 +0100 Subject: [PATCH 08/13] fix: feedbacks --- src/Error.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Error.js b/src/Error.js index 1744f1c303..166db1f265 100644 --- a/src/Error.js +++ b/src/Error.js @@ -8,7 +8,7 @@ import defaultLogger from './logger'; * @param {string} detailedMessage - The detailed error message to log server-side * @returns {Parse.Error} A Parse.Error with sanitized message */ -export function createSanitizedError(errorCode, detailedMessage) { +function createSanitizedError(errorCode, detailedMessage) { // On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file if (process.env.TESTING) { defaultLogger.error('Sanitized error:', detailedMessage); @@ -27,7 +27,7 @@ export function createSanitizedError(errorCode, detailedMessage) { * @param {string} detailedMessage - The detailed error message to log server-side * @returns {Error} An Error with sanitized message */ -export function createSanitizedHttpError(statusCode, detailedMessage) { +function createSanitizedHttpError(statusCode, detailedMessage) { // On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file if (process.env.TESTING) { defaultLogger.error('Sanitized error:', detailedMessage); @@ -41,3 +41,4 @@ export function createSanitizedHttpError(statusCode, detailedMessage) { return error; } +export { createSanitizedError, createSanitizedHttpError }; \ No newline at end of file From 767aba038ef0fdfd3d447c07e7a1a76424ac2af9 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:02:00 +0100 Subject: [PATCH 09/13] fix: lint --- src/Error.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Error.js b/src/Error.js index 166db1f265..4729965e1c 100644 --- a/src/Error.js +++ b/src/Error.js @@ -41,4 +41,4 @@ function createSanitizedHttpError(statusCode, detailedMessage) { return error; } -export { createSanitizedError, createSanitizedHttpError }; \ No newline at end of file +export { createSanitizedError, createSanitizedHttpError }; From 16cf793932939f63927313e848c210f1cd27a69f Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:15:00 +0100 Subject: [PATCH 10/13] fix: auto review --- spec/ParseInstallation.spec.js | 7 ++++++- spec/rest.spec.js | 38 ++++++++++++++++++++++++++++++---- spec/schemas.spec.js | 6 ++++++ spec/vulnerabilities.spec.js | 4 ++++ src/RestWrite.js | 4 ++-- 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index cfd55eb326..6bf007f473 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -7,6 +7,7 @@ const Config = require('../lib/Config'); const Parse = require('parse/node').Parse; const rest = require('../lib/rest'); const request = require('../lib/request'); +const { getSanitizedErrorCall } = require('../lib/TestUtils'); let config; let database; @@ -157,6 +158,9 @@ describe('Installations', () => { }); it('should properly fail queying installations', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); + const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'android'; const input = { @@ -174,10 +178,11 @@ describe('Installations', () => { done(); }) .catch(error => { - expect(error.code).toBe(119); + expect(error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(error.message).toBe( 'Permission denied' ); + sanitizedErrorCall.checkMessage("Clients aren't allowed to perform the find operation on the installation collection.", callCountBefore); done(); }); }); diff --git a/spec/rest.spec.js b/spec/rest.spec.js index 9b42739257..6743a10704 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -775,6 +775,8 @@ describe('rest create', () => { }); it('cannot get object in volatileClasses if not masterKey through pointer', async () => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); const masterKeyOnlyClassObject = new Parse.Object('_PushStatus'); await masterKeyOnlyClassObject.save(null, { useMasterKey: true }); const obj2 = new Parse.Object('TestObject'); @@ -788,9 +790,12 @@ describe('rest create', () => { await expectAsync(query.get(obj2.id)).toBeRejectedWithError( 'Permission denied' ); + sanitizedErrorCall.checkMessage("Clients aren't allowed to perform the get operation on the _PushStatus collection.", callCountBefore); }); it_id('3ce563bf-93aa-4d0b-9af9-c5fb246ac9fc')(it)('cannot get object in _GlobalConfig if not masterKey through pointer', async () => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); await Parse.Config.save({ privateData: 'secret' }, { privateData: true }); const obj2 = new Parse.Object('TestObject'); obj2.set('globalConfigPointer', { @@ -804,6 +809,7 @@ describe('rest create', () => { await expectAsync(query.get(obj2.id)).toBeRejectedWithError( 'Permission denied' ); + sanitizedErrorCall.checkMessage("Clients aren't allowed to perform the get operation on the _GlobalConfig collection.", callCountBefore); }); it('locks down session', done => { @@ -949,6 +955,8 @@ describe('rest update', () => { describe('read-only masterKey', () => { it('properly throws on rest.create, rest.update and rest.del', () => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); const config = Config.get('test'); const readOnly = auth.readOnly(config); expect(() => { @@ -959,6 +967,7 @@ describe('read-only masterKey', () => { 'Permission denied' ) ); + sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to perform the create operation.", callCountBefore); expect(() => { rest.update(config, readOnly, 'AnObject', {}); }).toThrow(); @@ -971,6 +980,8 @@ describe('read-only masterKey', () => { await reconfigureServer({ readOnlyMasterKey: 'yolo-read-only', }); + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); try { await request({ url: `${Parse.serverURL}/classes/MyYolo`, @@ -988,6 +999,7 @@ describe('read-only masterKey', () => { expect(res.data.error).toBe( 'Permission denied' ); + sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to perform the create operation.", callCountBefore); } await reconfigureServer(); }); @@ -1015,18 +1027,20 @@ describe('read-only masterKey', () => { }); it('should throw when trying to create RestWrite', () => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); const config = Config.get('test'); expect(() => { new RestWrite(config, auth.readOnly(config)); }).toThrow( - new Parse.Error( - Parse.Error.OPERATION_FORBIDDEN, - 'Cannot perform a write operation when using readOnlyMasterKey' - ) + new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied') ); + sanitizedErrorCall.checkMessage("Cannot perform a write operation when using readOnlyMasterKey", callCountBefore); }); it('should throw when trying to create schema', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); request({ method: 'POST', url: `${Parse.serverURL}/schemas`, @@ -1041,11 +1055,14 @@ describe('read-only masterKey', () => { .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); + sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to create a schema.", callCountBefore); done(); }); }); it('should throw when trying to create schema with a name', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'POST', @@ -1060,11 +1077,14 @@ describe('read-only masterKey', () => { .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); + sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to create a schema.", callCountBefore); done(); }); }); it('should throw when trying to update schema', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'PUT', @@ -1079,11 +1099,14 @@ describe('read-only masterKey', () => { .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); + sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to update a schema.", callCountBefore); done(); }); }); it('should throw when trying to delete schema', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'DELETE', @@ -1098,11 +1121,14 @@ describe('read-only masterKey', () => { .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); + sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to delete a schema.", callCountBefore); done(); }); }); it('should throw when trying to update the global config', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); request({ url: `${Parse.serverURL}/config`, method: 'PUT', @@ -1117,11 +1143,14 @@ describe('read-only masterKey', () => { .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); + sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to update the config.", callCountBefore); done(); }); }); it('should throw when trying to send push', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); request({ url: `${Parse.serverURL}/push`, method: 'POST', @@ -1138,6 +1167,7 @@ describe('read-only masterKey', () => { expect(res.data.error).toBe( 'Permission denied' ); + sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to send push notifications.", callCountBefore); done(); }); }); diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 83f59cf83f..dbbc9f71e2 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -168,6 +168,8 @@ describe('schemas', () => { }); it('requires the master key to get one schema', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); request({ url: 'http://localhost:8378/1/schemas/SomeSchema', json: true, @@ -175,11 +177,14 @@ describe('schemas', () => { }).then(fail, response => { expect(response.status).toEqual(403); expect(response.data.error).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage("unauthorized: master key is required", callCountBefore); done(); }); }); it('asks for the master key if you use the rest key', done => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); request({ url: 'http://localhost:8378/1/schemas', json: true, @@ -187,6 +192,7 @@ describe('schemas', () => { }).then(fail, response => { expect(response.status).toEqual(403); expect(response.data.error).toEqual('Permission denied'); + sanitizedErrorCall.checkMessage("unauthorized: master key is required", callCountBefore); done(); }); }); diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index f270400424..8e61439563 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -1,4 +1,5 @@ const request = require('../lib/request'); +const { getSanitizedErrorCall } = require('../lib/TestUtils'); describe('Vulnerabilities', () => { describe('(GHSA-8xq9-g7ch-35hg) Custom object ID allows to acquire role privilege', () => { @@ -13,9 +14,12 @@ describe('Vulnerabilities', () => { }); it('denies user creation with poisoned object ID', async () => { + const sanitizedErrorCall = getSanitizedErrorCall(); + const callCountBefore = sanitizedErrorCall.callCountBefore(); await expectAsync( new Parse.User({ id: 'role:a', username: 'a', password: '123' }).save() ).toBeRejectedWith(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')); + sanitizedErrorCall.checkMessage("Invalid object ID.", callCountBefore); }); describe('existing sessions for users with poisoned object ID', () => { diff --git a/src/RestWrite.js b/src/RestWrite.js index 447af1d450..c8c97a97e0 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -31,9 +31,9 @@ import defaultLogger from './logger'; // for the _User class. function RestWrite(config, auth, className, query, data, originalData, clientSDK, context, action) { if (auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, - 'Cannot perform a write operation when using readOnlyMasterKey' + 'Cannot perform a write operation when using readOnlyMasterKey', ); } this.config = config; From bbb007f1320ff9e74a95722a1f76c9d0f0d123f1 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:33:53 +0100 Subject: [PATCH 11/13] fix: feedbacks --- spec/AudienceRouter.spec.js | 41 +++++++------- spec/LogsRouter.spec.js | 9 ++- spec/ParseAPI.spec.js | 9 +-- spec/ParseFile.spec.js | 21 ++++--- spec/ParseGlobalConfig.spec.js | 9 ++- spec/ParseGraphQLServer.spec.js | 55 ++++++++++--------- spec/ParseInstallation.spec.js | 8 +-- spec/ParseQuery.Aggregate.spec.js | 9 ++- spec/ParseUser.spec.js | 33 ++++++----- spec/RestQuery.spec.js | 8 +-- spec/Schema.spec.js | 41 +++++++------- spec/features.spec.js | 9 ++- spec/rest.spec.js | 85 ++++++++++++++++------------- spec/schemas.spec.js | 69 ++++++++++++----------- spec/vulnerabilities.spec.js | 8 +-- src/GraphQL/loaders/usersQueries.js | 5 +- src/Routers/UsersRouter.js | 4 +- src/TestUtils.js | 20 ------- src/rest.js | 3 +- 19 files changed, 223 insertions(+), 223 deletions(-) diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js index c191adf57a..9ad10b4c1d 100644 --- a/spec/AudienceRouter.spec.js +++ b/spec/AudienceRouter.spec.js @@ -2,7 +2,6 @@ const auth = require('../lib/Auth'); const Config = require('../lib/Config'); const rest = require('../lib/rest'); const request = require('../lib/request'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); const AudiencesRouter = require('../lib/Routers/AudiencesRouter').AudiencesRouter; describe('AudiencesRouter', () => { @@ -264,9 +263,9 @@ describe('AudiencesRouter', () => { }); it('should only create with master key', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); Parse._request('POST', 'push_audiences', { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' }), @@ -274,65 +273,65 @@ describe('AudiencesRouter', () => { () => {}, error => { expect(error.message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); }); it('should only find with master key', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); Parse._request('GET', 'push_audiences', {}).then( () => {}, error => { expect(error.message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); }); it('should only get with master key', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); Parse._request('GET', `push_audiences/someId`, {}).then( () => {}, error => { expect(error.message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); }); it('should only update with master key', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); Parse._request('PUT', `push_audiences/someId`, { name: 'My Audience 2', }).then( () => {}, error => { expect(error.message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); }); it('should only delete with master key', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); Parse._request('DELETE', `push_audiences/someId`, {}).then( () => {}, error => { expect(error.message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); diff --git a/spec/LogsRouter.spec.js b/spec/LogsRouter.spec.js index 502e005676..d4b77baaa8 100644 --- a/spec/LogsRouter.spec.js +++ b/spec/LogsRouter.spec.js @@ -1,7 +1,6 @@ 'use strict'; const request = require('../lib/request'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); const LogsRouter = require('../lib/Routers/LogsRouter').LogsRouter; const LoggerController = require('../lib/Controllers/LoggerController').LoggerController; const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter') @@ -53,9 +52,9 @@ describe_only(() => { }); it('can check invalid master key of request', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); request({ url: 'http://localhost:8378/1/scriptlog', headers: { @@ -66,7 +65,7 @@ describe_only(() => { const body = response.data; expect(response.status).toEqual(403); expect(body.error).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); }); }); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index b7282647e7..779a97c9f2 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -6,7 +6,7 @@ const request = require('../lib/request'); const Parse = require('parse/node'); const Config = require('../lib/Config'); const SchemaController = require('../lib/Controllers/SchemaController'); -const { getSanitizedErrorCall, destroyAllDataPermanently } = require('../lib/TestUtils'); +const { destroyAllDataPermanently } = require('../lib/TestUtils'); const userSchema = SchemaController.convertSchemaToAdapterSchema({ className: '_User', @@ -1710,14 +1710,15 @@ describe('miscellaneous', () => { }); it('fail on purge all objects in class without master key', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', }; - const callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); request({ method: 'DELETE', headers: headers, @@ -1728,7 +1729,7 @@ describe('miscellaneous', () => { }) .catch(response => { expect(response.data.error).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); }); }); diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 8e9bf7d9c2..1495ab67a9 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -5,7 +5,6 @@ const { FilesController } = require('../lib/Controllers/FilesController'); const request = require('../lib/request'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); const str = 'Hello World!'; const data = []; @@ -133,7 +132,8 @@ describe('Parse.File testing', () => { }); it('blocks file deletions with missing or incorrect master-key header', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); const headers = { 'Content-Type': 'image/jpeg', @@ -149,7 +149,7 @@ describe('Parse.File testing', () => { const b = response.data; expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/); // missing X-Parse-Master-Key header - const callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); request({ method: 'DELETE', headers: { @@ -161,9 +161,9 @@ describe('Parse.File testing', () => { const del_b = response.data; expect(response.status).toEqual(403); expect(del_b.error).toBe('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); // incorrect X-Parse-Master-Key header - const callCountBefore2 = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); request({ method: 'DELETE', headers: { @@ -176,7 +176,7 @@ describe('Parse.File testing', () => { const del_b2 = response.data; expect(response.status).toEqual(403); expect(del_b2.error).toBe('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore2); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); }); }); @@ -763,16 +763,15 @@ describe('Parse.File testing', () => { describe('getting files', () => { it('does not crash on file request with invalid app ID', async () => { - const { getSanitizedErrorCall } = require('../lib/TestUtils'); - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); const res1 = await request({ url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt', }).catch(e => e); expect(res1.status).toBe(403); expect(res1.data).toEqual({ code: 119, error: 'Permission denied' }); - sanitizedErrorCall.checkMessage('Invalid application ID.', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid application ID.')); // Ensure server did not crash const res2 = await request({ url: 'http://localhost:8378/1/health' }); expect(res2.status).toEqual(200); diff --git a/spec/ParseGlobalConfig.spec.js b/spec/ParseGlobalConfig.spec.js index 9ae5130d54..1b3a9adc0d 100644 --- a/spec/ParseGlobalConfig.spec.js +++ b/spec/ParseGlobalConfig.spec.js @@ -2,7 +2,6 @@ const request = require('../lib/request'); const Config = require('../lib/Config'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); describe('a GlobalConfig', () => { beforeEach(async () => { @@ -221,9 +220,9 @@ describe('a GlobalConfig', () => { }); it('fail to update if master key is missing', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); request({ method: 'PUT', url: 'http://localhost:8378/1/config', @@ -238,7 +237,7 @@ describe('a GlobalConfig', () => { const body = response.data; expect(response.status).toEqual(403); expect(body.error).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); }); }); diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 22c8b50a16..411e0f9798 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -35,7 +35,6 @@ const { ParseServer } = require('../'); const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer'); const { ReadPreference, Collection } = require('mongodb'); const { v4: uuidv4 } = require('uuid'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); function handleError(e) { if (e && e.networkError && e.networkError.result && e.networkError.result.errors) { @@ -3489,9 +3488,9 @@ describe('ParseGraphQLServer', () => { }); it('should require master key to create a new class', async () => { - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); try { await apolloClient.mutate({ mutation: gql` @@ -3506,7 +3505,7 @@ describe('ParseGraphQLServer', () => { } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(e.graphQLErrors[0].message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); @@ -3863,9 +3862,9 @@ describe('ParseGraphQLServer', () => { handleError(e); } - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); try { await apolloClient.mutate({ mutation: gql` @@ -3880,7 +3879,7 @@ describe('ParseGraphQLServer', () => { } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(e.graphQLErrors[0].message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); @@ -4092,10 +4091,9 @@ describe('ParseGraphQLServer', () => { handleError(e); } - const { getSanitizedErrorCall } = require('../lib/TestUtils'); - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); try { await apolloClient.mutate({ mutation: gql` @@ -4110,7 +4108,7 @@ describe('ParseGraphQLServer', () => { } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(e.graphQLErrors[0].message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); @@ -4138,10 +4136,9 @@ describe('ParseGraphQLServer', () => { }); it('should require master key to get an existing class', async () => { - const { getSanitizedErrorCall } = require('../lib/TestUtils'); - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); try { await apolloClient.query({ query: gql` @@ -4156,15 +4153,14 @@ describe('ParseGraphQLServer', () => { } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(e.graphQLErrors[0].message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); it('should require master key to find the existing classes', async () => { - const { getSanitizedErrorCall } = require('../lib/TestUtils'); - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); try { await apolloClient.query({ query: gql` @@ -4179,7 +4175,7 @@ describe('ParseGraphQLServer', () => { } catch (e) { expect(e.graphQLErrors[0].extensions.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(e.graphQLErrors[0].message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); }); @@ -7805,6 +7801,8 @@ describe('ParseGraphQLServer', () => { }); it('should fail due to empty session token', async () => { + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); try { await apolloClient.query({ query: gql` @@ -7826,7 +7824,8 @@ describe('ParseGraphQLServer', () => { } catch (err) { const { graphQLErrors } = err; expect(graphQLErrors.length).toBe(1); - expect(graphQLErrors[0].message).toBe('Invalid session token'); + expect(graphQLErrors[0].message).toBe('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid session token')); } }); @@ -7836,6 +7835,9 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + try { await apolloClient.query({ query: gql` @@ -7864,7 +7866,8 @@ describe('ParseGraphQLServer', () => { } catch (err) { const { graphQLErrors } = err; expect(graphQLErrors.length).toBe(1); - expect(graphQLErrors[0].message).toBe('Invalid session token'); + expect(graphQLErrors[0].message).toBe('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid session token')); } }); }); diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index 6bf007f473..408e8fa7bf 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -7,7 +7,6 @@ const Config = require('../lib/Config'); const Parse = require('parse/node').Parse; const rest = require('../lib/rest'); const request = require('../lib/request'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); let config; let database; @@ -158,8 +157,8 @@ describe('Installations', () => { }); it('should properly fail queying installations', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'android'; @@ -170,6 +169,7 @@ describe('Installations', () => { rest .create(config, auth.nobody(config), '_Installation', input) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query(Parse.Installation); return query.find(); }) @@ -182,7 +182,7 @@ describe('Installations', () => { expect(error.message).toBe( 'Permission denied' ); - sanitizedErrorCall.checkMessage("Clients aren't allowed to perform the find operation on the installation collection.", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the find operation on the installation collection.")); done(); }); }); diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index 0f431c31c7..eb9c03ac4e 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -2,7 +2,6 @@ const Parse = require('parse/node'); const request = require('../lib/request'); const Config = require('../lib/Config'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); const masterKeyHeaders = { 'X-Parse-Application-Id': 'test', @@ -75,14 +74,14 @@ describe('Parse.Query Aggregate testing', () => { }); it('should only query aggregate with master key', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); Parse._request('GET', `aggregate/someClass`, {}).then( () => {}, error => { expect(error.message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 702c3851de..42ac77ac62 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -12,7 +12,10 @@ const request = require('../lib/request'); const passwordCrypto = require('../lib/password'); const Config = require('../lib/Config'); const cryptoUtils = require('../lib/cryptoUtils'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); + + + + describe('allowExpiredAuthDataToken option', () => { it('should accept true value', async () => { @@ -2633,8 +2636,9 @@ describe('Parse.User testing', () => { }); it('cannot delete session if no sessionToken', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - let callCountBefore = 0; + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + Promise.resolve() .then(() => { return Parse.User.signUp('test1', 'test', { foo: 'bar' }); @@ -2654,7 +2658,7 @@ describe('Parse.User testing', () => { const b = response.data; expect(b.results.length).toEqual(1); const objId = b.results[0].objectId; - callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); request({ method: 'DELETE', headers: { @@ -2666,7 +2670,8 @@ describe('Parse.User testing', () => { const b = response.data; expect(b.code).toEqual(209); expect(b.error).toBe('Permission denied'); - sanitizedErrorCall.checkMessage('Invalid session token', callCountBefore); + + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid session token')); done(); }); }); @@ -3360,8 +3365,8 @@ describe('Parse.User testing', () => { sendMail: () => Promise.resolve(), }; - let sanitizedErrorCall; - let callCountBefore = 0; + let logger; + let loggerErrorSpy; const user = new Parse.User(); user.set({ @@ -3377,11 +3382,12 @@ describe('Parse.User testing', () => { publicServerURL: 'http://localhost:8378/1', }) .then(() => { - sanitizedErrorCall = getSanitizedErrorCall(); + logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); return user.signUp(); }) .then(() => { - callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); return Parse.User.current().set('emailVerified', true).save(); }) .then(() => { @@ -3390,7 +3396,7 @@ describe('Parse.User testing', () => { }) .catch(err => { expect(err.message).toBe('Permission denied'); - sanitizedErrorCall.checkMessage("Clients aren't allowed to manually update email verification.", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to manually update email verification.")); done(); }); @@ -4384,13 +4390,14 @@ describe('login as other user', () => { }); it('rejects creating a session for another user without the master key', async done => { - const sanitizedErrorCall = getSanitizedErrorCall(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); await Parse.User.signUp('some_user', 'some_password'); const userId = Parse.User.current().id; await Parse.User.logOut(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); try { await request({ method: 'POST', @@ -4409,7 +4416,7 @@ describe('login as other user', () => { } catch (err) { expect(err.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(err.data.error).toBe('Permission denied'); - sanitizedErrorCall.checkMessage('master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('master key is required')); } const sessionsQuery = new Parse.Query(Parse.Session); diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 880ef79f5d..fb5370d759 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -5,7 +5,6 @@ const Config = require('../lib/Config'); const rest = require('../lib/rest'); const RestQuery = require('../lib/RestQuery'); const request = require('../lib/request'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); const querystring = require('querystring'); let config; @@ -155,12 +154,13 @@ describe('rest query', () => { }); it('query non-existent class when disabled client class creation', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); const customConfig = Object.assign({}, config, { allowClientClassCreation: false, }); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}).then( () => { fail('Should throw an error'); @@ -169,7 +169,7 @@ describe('rest query', () => { err => { expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(err.message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('This user is not allowed to access ' + 'non-existent class: ClientClassCreation', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('This user is not allowed to access ' + 'non-existent class: ClientClassCreation')); done(); } ); diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index fcf571f67b..40b9bf769c 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -2,7 +2,6 @@ const Config = require('../lib/Config'); const SchemaController = require('../lib/Controllers/SchemaController'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); const dd = require('deep-diff'); let config; @@ -250,8 +249,8 @@ describe('SchemaController', () => { }); it('class-level permissions test count', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - let callCountBefore = 0; + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); let obj; return ( @@ -279,7 +278,7 @@ describe('SchemaController', () => { }) .then(results => { expect(results.length).toBe(1); - callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('Stuff'); return query.count(); }) @@ -290,7 +289,7 @@ describe('SchemaController', () => { err => { expect(err.message).toEqual('Permission denied'); expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - sanitizedErrorCall.checkMessage('Permission denied for action count on class Stuff', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action count on class Stuff')); done(); } ) @@ -1446,8 +1445,8 @@ describe('Class Level Permissions for requiredAuth', () => { } it('required auth test find', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - let callCountBefore = 0; + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); config.database .loadSchema() @@ -1463,7 +1462,7 @@ describe('Class Level Permissions for requiredAuth', () => { }); }) .then(() => { - callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('Stuff'); return query.find(); }) @@ -1474,7 +1473,7 @@ describe('Class Level Permissions for requiredAuth', () => { }, e => { expect(e.message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('Permission denied, user needs to be authenticated.', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.')); done(); } ); @@ -1549,8 +1548,8 @@ describe('Class Level Permissions for requiredAuth', () => { }); it('required auth should reject create when not authenticated', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - let callCountBefore = 0; + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); config.database .loadSchema() .then(schema => { @@ -1565,7 +1564,7 @@ describe('Class Level Permissions for requiredAuth', () => { }); }) .then(() => { - callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff.save(); @@ -1577,7 +1576,7 @@ describe('Class Level Permissions for requiredAuth', () => { }, e => { expect(e.message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('Permission denied, user needs to be authenticated.', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.')); done(); } ); @@ -1635,8 +1634,8 @@ describe('Class Level Permissions for requiredAuth', () => { }); it('required auth test get not authenticated', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - let callCountBefore = 0; + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); config.database .loadSchema() @@ -1658,7 +1657,7 @@ describe('Class Level Permissions for requiredAuth', () => { const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff.save().then(() => { - callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('Stuff'); return query.get(stuff.id); }); @@ -1670,15 +1669,15 @@ describe('Class Level Permissions for requiredAuth', () => { }, e => { expect(e.message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('Permission denied, user needs to be authenticated.', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.')); done(); } ); }); it('required auth test find not authenticated', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - let callCountBefore = 0; + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); config.database .loadSchema() @@ -1709,7 +1708,7 @@ describe('Class Level Permissions for requiredAuth', () => { }) .then(result => { expect(result.get('foo')).toEqual('bar'); - callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('Stuff'); return query.find(); }) @@ -1720,7 +1719,7 @@ describe('Class Level Permissions for requiredAuth', () => { }, e => { expect(e.message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('Permission denied, user needs to be authenticated.', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.')); done(); } ); diff --git a/spec/features.spec.js b/spec/features.spec.js index 20ee1789a1..201e01293d 100644 --- a/spec/features.spec.js +++ b/spec/features.spec.js @@ -1,7 +1,6 @@ 'use strict'; const request = require('../lib/request'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); describe('features', () => { it('should return the serverInfo', async () => { @@ -21,9 +20,9 @@ describe('features', () => { }); it('requires the master key to get features', async done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); try { await request({ url: 'http://localhost:8378/1/serverInfo', @@ -37,7 +36,7 @@ describe('features', () => { } catch (error) { expect(error.status).toEqual(403); expect(error.data.error).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('unauthorized: master key is required', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } }); diff --git a/spec/rest.spec.js b/spec/rest.spec.js index 6743a10704..cd0dddd624 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -6,7 +6,6 @@ const Parse = require('parse/node').Parse; const rest = require('../lib/rest'); const RestWrite = require('../lib/RestWrite'); const request = require('../lib/request'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); let config; let database; @@ -315,12 +314,13 @@ describe('rest create', () => { }); it('handles create on non-existent class when disabled client class creation', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); const customConfig = Object.assign({}, config, { allowClientClassCreation: false, }); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}).then( () => { fail('Should throw an error'); @@ -329,7 +329,7 @@ describe('rest create', () => { err => { expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); expect(err.message).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage('This user is not allowed to access ' + 'non-existent class: ClientClassCreation', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('This user is not allowed to access ' + 'non-existent class: ClientClassCreation')); done(); } ); @@ -775,8 +775,9 @@ describe('rest create', () => { }); it('cannot get object in volatileClasses if not masterKey through pointer', async () => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); const masterKeyOnlyClassObject = new Parse.Object('_PushStatus'); await masterKeyOnlyClassObject.save(null, { useMasterKey: true }); const obj2 = new Parse.Object('TestObject'); @@ -790,12 +791,13 @@ describe('rest create', () => { await expectAsync(query.get(obj2.id)).toBeRejectedWithError( 'Permission denied' ); - sanitizedErrorCall.checkMessage("Clients aren't allowed to perform the get operation on the _PushStatus collection.", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the get operation on the _PushStatus collection.")); }); it_id('3ce563bf-93aa-4d0b-9af9-c5fb246ac9fc')(it)('cannot get object in _GlobalConfig if not masterKey through pointer', async () => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); await Parse.Config.save({ privateData: 'secret' }, { privateData: true }); const obj2 = new Parse.Object('TestObject'); obj2.set('globalConfigPointer', { @@ -809,7 +811,7 @@ describe('rest create', () => { await expectAsync(query.get(obj2.id)).toBeRejectedWithError( 'Permission denied' ); - sanitizedErrorCall.checkMessage("Clients aren't allowed to perform the get operation on the _GlobalConfig collection.", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the get operation on the _GlobalConfig collection.")); }); it('locks down session', done => { @@ -955,8 +957,9 @@ describe('rest update', () => { describe('read-only masterKey', () => { it('properly throws on rest.create, rest.update and rest.del', () => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); const config = Config.get('test'); const readOnly = auth.readOnly(config); expect(() => { @@ -967,7 +970,7 @@ describe('read-only masterKey', () => { 'Permission denied' ) ); - sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to perform the create operation.", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to perform the create operation.")); expect(() => { rest.update(config, readOnly, 'AnObject', {}); }).toThrow(); @@ -980,8 +983,9 @@ describe('read-only masterKey', () => { await reconfigureServer({ readOnlyMasterKey: 'yolo-read-only', }); - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); try { await request({ url: `${Parse.serverURL}/classes/MyYolo`, @@ -999,7 +1003,7 @@ describe('read-only masterKey', () => { expect(res.data.error).toBe( 'Permission denied' ); - sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to perform the create operation.", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to perform the create operation.")); } await reconfigureServer(); }); @@ -1027,20 +1031,22 @@ describe('read-only masterKey', () => { }); it('should throw when trying to create RestWrite', () => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); const config = Config.get('test'); expect(() => { new RestWrite(config, auth.readOnly(config)); }).toThrow( new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied') ); - sanitizedErrorCall.checkMessage("Cannot perform a write operation when using readOnlyMasterKey", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Cannot perform a write operation when using readOnlyMasterKey")); }); it('should throw when trying to create schema', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); request({ method: 'POST', url: `${Parse.serverURL}/schemas`, @@ -1055,14 +1061,15 @@ describe('read-only masterKey', () => { .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); - sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to create a schema.", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to create a schema.")); done(); }); }); it('should throw when trying to create schema with a name', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'POST', @@ -1077,14 +1084,15 @@ describe('read-only masterKey', () => { .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); - sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to create a schema.", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to create a schema.")); done(); }); }); it('should throw when trying to update schema', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'PUT', @@ -1099,14 +1107,15 @@ describe('read-only masterKey', () => { .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); - sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to update a schema.", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to update a schema.")); done(); }); }); it('should throw when trying to delete schema', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'DELETE', @@ -1121,14 +1130,15 @@ describe('read-only masterKey', () => { .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); - sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to delete a schema.", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to delete a schema.")); done(); }); }); it('should throw when trying to update the global config', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/config`, method: 'PUT', @@ -1143,14 +1153,15 @@ describe('read-only masterKey', () => { .catch(res => { expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); expect(res.data.error).toBe('Permission denied'); - sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to update the config.", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to update the config.")); done(); }); }); it('should throw when trying to send push', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/push`, method: 'POST', @@ -1167,7 +1178,7 @@ describe('read-only masterKey', () => { expect(res.data.error).toBe( 'Permission denied' ); - sanitizedErrorCall.checkMessage("read-only masterKey isn't allowed to send push notifications.", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to send push notifications.")); done(); }); }); diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index dbbc9f71e2..3484f207fb 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -5,7 +5,6 @@ const dd = require('deep-diff'); const Config = require('../lib/Config'); const request = require('../lib/request'); const TestUtils = require('../lib/TestUtils'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); const SchemaController = require('../lib/Controllers/SchemaController').SchemaController; let config; @@ -168,8 +167,9 @@ describe('schemas', () => { }); it('requires the master key to get one schema', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); request({ url: 'http://localhost:8378/1/schemas/SomeSchema', json: true, @@ -177,14 +177,15 @@ describe('schemas', () => { }).then(fail, response => { expect(response.status).toEqual(403); expect(response.data.error).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage("unauthorized: master key is required", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("unauthorized: master key is required")); done(); }); }); it('asks for the master key if you use the rest key', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); request({ url: 'http://localhost:8378/1/schemas', json: true, @@ -192,7 +193,7 @@ describe('schemas', () => { }).then(fail, response => { expect(response.status).toEqual(403); expect(response.data.error).toEqual('Permission denied'); - sanitizedErrorCall.checkMessage("unauthorized: master key is required", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("unauthorized: master key is required")); done(); }); }); @@ -1814,7 +1815,8 @@ describe('schemas', () => { }); it('should not be able to add a field', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); request({ method: 'POST', @@ -1835,7 +1837,7 @@ describe('schemas', () => { }, }, }).then(() => { - const callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); const object = new Parse.Object('AClass'); object.set('hello', 'world'); return object.save().then( @@ -1846,7 +1848,7 @@ describe('schemas', () => { err => { expect(err.message).toEqual('Permission denied'); expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - sanitizedErrorCall.checkMessage('Permission denied for action addField on class AClass', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action addField on class AClass')); done(); } ); @@ -2179,7 +2181,8 @@ describe('schemas', () => { } it('validate CLP 1', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); const user = new Parse.User(); user.setUsername('user'); @@ -2212,7 +2215,7 @@ describe('schemas', () => { }); }) .then(() => { - const callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { @@ -2221,7 +2224,7 @@ describe('schemas', () => { err => { expect(err.message).toEqual('Permission denied'); expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - sanitizedErrorCall.checkMessage('Permission denied for action find on class AClass', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2244,8 +2247,8 @@ describe('schemas', () => { }); it('validate CLP 2', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - let callCountBefore = 0; + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); const user = new Parse.User(); user.setUsername('user'); @@ -2278,7 +2281,7 @@ describe('schemas', () => { }); }) .then(() => { - callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { @@ -2287,7 +2290,7 @@ describe('schemas', () => { err => { expect(err.message).toEqual('Permission denied'); expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - sanitizedErrorCall.checkMessage('Permission denied for action find on class AClass', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2335,8 +2338,8 @@ describe('schemas', () => { }); it('validate CLP 3', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - let callCountBefore = 0; + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); const user = new Parse.User(); user.setUsername('user'); @@ -2369,7 +2372,7 @@ describe('schemas', () => { }); }) .then(() => { - callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { @@ -2378,7 +2381,7 @@ describe('schemas', () => { err => { expect(err.message).toEqual('Permission denied'); expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - sanitizedErrorCall.checkMessage('Permission denied for action find on class AClass', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2417,8 +2420,8 @@ describe('schemas', () => { }); it('validate CLP 4', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - let callCountBefore = 0; + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); const user = new Parse.User(); user.setUsername('user'); @@ -2451,7 +2454,7 @@ describe('schemas', () => { }); }) .then(() => { - callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { @@ -2460,7 +2463,7 @@ describe('schemas', () => { err => { expect(err.message).toEqual('Permission denied'); expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - sanitizedErrorCall.checkMessage('Permission denied for action find on class AClass', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2485,7 +2488,7 @@ describe('schemas', () => { ); }) .then(() => { - callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { @@ -2494,7 +2497,7 @@ describe('schemas', () => { err => { expect(err.message).toEqual('Permission denied'); expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - sanitizedErrorCall.checkMessage('Permission denied for action find on class AClass', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2517,8 +2520,8 @@ describe('schemas', () => { }); it('validate CLP 5', done => { - const sanitizedErrorCall = getSanitizedErrorCall(); - let callCountBefore = 0; + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); const user = new Parse.User(); user.setUsername('user'); @@ -2572,7 +2575,7 @@ describe('schemas', () => { return Parse.User.logIn('admin', 'admin'); }) .then(() => { - callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find(); }) @@ -2584,7 +2587,7 @@ describe('schemas', () => { err => { expect(err.message).toEqual('Permission denied'); expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - sanitizedErrorCall.checkMessage('Permission denied for action create on class AClass', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action create on class AClass')); return Promise.resolve(); } ) @@ -2592,7 +2595,7 @@ describe('schemas', () => { return Parse.User.logIn('user2', 'user2'); }) .then(() => { - callCountBefore = sanitizedErrorCall.callCountBefore(); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find(); }) @@ -2604,7 +2607,7 @@ describe('schemas', () => { err => { expect(err.message).toEqual('Permission denied'); expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - sanitizedErrorCall.checkMessage('Permission denied for action find on class AClass', callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ) diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index 8e61439563..f3aff6ef0b 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -1,5 +1,4 @@ const request = require('../lib/request'); -const { getSanitizedErrorCall } = require('../lib/TestUtils'); describe('Vulnerabilities', () => { describe('(GHSA-8xq9-g7ch-35hg) Custom object ID allows to acquire role privilege', () => { @@ -14,12 +13,13 @@ describe('Vulnerabilities', () => { }); it('denies user creation with poisoned object ID', async () => { - const sanitizedErrorCall = getSanitizedErrorCall(); - const callCountBefore = sanitizedErrorCall.callCountBefore(); + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); await expectAsync( new Parse.User({ id: 'role:a', username: 'a', password: '123' }).save() ).toBeRejectedWith(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')); - sanitizedErrorCall.checkMessage("Invalid object ID.", callCountBefore); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Invalid object ID.")); }); describe('existing sessions for users with poisoned object ID', () => { diff --git a/src/GraphQL/loaders/usersQueries.js b/src/GraphQL/loaders/usersQueries.js index c64ce6b90d..a51e9553c0 100644 --- a/src/GraphQL/loaders/usersQueries.js +++ b/src/GraphQL/loaders/usersQueries.js @@ -4,11 +4,12 @@ import Parse from 'parse/node'; import rest from '../../rest'; import { extractKeysAndInclude } from './parseClassTypes'; import { Auth } from '../../Auth'; +import { createSanitizedError } from '../../Error'; const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) => { const { info, config } = context; if (!info || !info.sessionToken) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } const sessionToken = info.sessionToken; const selectedFields = getFieldNames(queryInfo) @@ -62,7 +63,7 @@ const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) = info.context ); if (!response.results || response.results.length == 0) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } else { const user = response.results[0]; return { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index e1db8dd9ec..64e7f7371b 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -171,7 +171,7 @@ export class UsersRouter extends ClassesRouter { handleMe(req) { if (!req.info || !req.info.sessionToken) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } const sessionToken = req.info.sessionToken; return rest @@ -186,7 +186,7 @@ export class UsersRouter extends ClassesRouter { ) .then(response => { if (!response.results || response.results.length == 0 || !response.results[0].user) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } else { const user = response.results[0].user; // Send token back on the login, because SDKs expect that. diff --git a/src/TestUtils.js b/src/TestUtils.js index 33ec2b214a..ec4cb29554 100644 --- a/src/TestUtils.js +++ b/src/TestUtils.js @@ -82,23 +82,3 @@ export class Connections { } } -export function getSanitizedErrorCall() { - const logger = require('../lib/logger').default; - // eslint-disable-next-line no-undef - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - - return { - callCountBefore: () => loggerErrorSpy.calls.count(), - checkMessage: (message, callCountBefore) => { - // eslint-disable-next-line no-undef - expect(loggerErrorSpy.calls.count()).toBeGreaterThan(callCountBefore); - const calls = loggerErrorSpy.calls.all(); - const recentCalls = calls.slice(callCountBefore); - const sanitizedErrorCall = recentCalls.find(call => call.args[0] === 'Sanitized error:'); - // eslint-disable-next-line no-undef - expect(sanitizedErrorCall).toBeDefined(); - // eslint-disable-next-line no-undef - expect(sanitizedErrorCall.args[1]).toContain(message); - }, - }; -} diff --git a/src/rest.js b/src/rest.js index 933f6d7487..b28a7d5a40 100644 --- a/src/rest.js +++ b/src/rest.js @@ -13,6 +13,7 @@ var RestQuery = require('./RestQuery'); var RestWrite = require('./RestWrite'); var triggers = require('./triggers'); const { enforceRoleSecurity } = require('./SharedRest'); +const { createSanitizedError } = require('./Error'); function checkTriggers(className, config, types) { return types.some(triggerType => { @@ -195,7 +196,7 @@ function del(config, auth, className, objectId, context) { firstResult.className = className; if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) { if (!auth.user || firstResult.user.objectId !== auth.user.id) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } } var cacheAdapter = config.cacheController; From 7f282c3d9c3e71847734658dc9dcf42eba51258d Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:48:17 +0100 Subject: [PATCH 12/13] fix: lint --- spec/ParseUser.spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 42ac77ac62..4763643d79 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -14,9 +14,6 @@ const Config = require('../lib/Config'); const cryptoUtils = require('../lib/cryptoUtils'); - - - describe('allowExpiredAuthDataToken option', () => { it('should accept true value', async () => { await reconfigureServer({ allowExpiredAuthDataToken: true }); From 70382e848358fb301aad56cdc2a5a0ef0ff61957 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:53:00 +0100 Subject: [PATCH 13/13] fix: feedbacks --- spec/AudienceRouter.spec.js | 17 ++++++------- spec/ParseFile.spec.js | 12 ++++++---- spec/ParseGraphQLServer.spec.js | 20 ++++------------ spec/ParseUser.spec.js | 18 +++++++++----- spec/Schema.spec.js | 22 +++++++---------- spec/rest.spec.js | 42 +++++++++++++-------------------- spec/schemas.spec.js | 27 ++++----------------- 7 files changed, 60 insertions(+), 98 deletions(-) diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js index 9ad10b4c1d..f6d6af9393 100644 --- a/spec/AudienceRouter.spec.js +++ b/spec/AudienceRouter.spec.js @@ -5,6 +5,13 @@ const request = require('../lib/request'); const AudiencesRouter = require('../lib/Routers/AudiencesRouter').AudiencesRouter; describe('AudiencesRouter', () => { + let loggerErrorSpy; + + beforeEach(() => { + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + }); + it('uses find condition from request.body', done => { const config = Config.get('test'); const androidAudienceRequest = { @@ -263,8 +270,6 @@ describe('AudiencesRouter', () => { }); it('should only create with master key', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); Parse._request('POST', 'push_audiences', { name: 'My Audience', @@ -280,8 +285,6 @@ describe('AudiencesRouter', () => { }); it('should only find with master key', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); Parse._request('GET', 'push_audiences', {}).then( () => {}, @@ -294,8 +297,6 @@ describe('AudiencesRouter', () => { }); it('should only get with master key', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); Parse._request('GET', `push_audiences/someId`, {}).then( () => {}, @@ -308,8 +309,6 @@ describe('AudiencesRouter', () => { }); it('should only update with master key', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); Parse._request('PUT', `push_audiences/someId`, { name: 'My Audience 2', @@ -324,8 +323,6 @@ describe('AudiencesRouter', () => { }); it('should only delete with master key', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); Parse._request('DELETE', `push_audiences/someId`, {}).then( () => {}, diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 1495ab67a9..7e6f5e5080 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -13,6 +13,13 @@ for (let i = 0; i < str.length; i++) { } describe('Parse.File testing', () => { + let loggerErrorSpy; + + beforeEach(() => { + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + }); + describe('creating files', () => { it('works with Content-Type', done => { const headers = { @@ -132,9 +139,6 @@ describe('Parse.File testing', () => { }); it('blocks file deletions with missing or incorrect master-key header', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - const headers = { 'Content-Type': 'image/jpeg', 'X-Parse-Application-Id': 'test', @@ -763,8 +767,6 @@ describe('Parse.File testing', () => { describe('getting files', () => { it('does not crash on file request with invalid app ID', async () => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); const res1 = await request({ url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt', diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 411e0f9798..aa57e973ef 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -47,6 +47,8 @@ function handleError(e) { describe('ParseGraphQLServer', () => { let parseServer; let parseGraphQLServer; + let loggerErrorSpy; + beforeEach(async () => { parseServer = await global.reconfigureServer({ @@ -58,6 +60,9 @@ describe('ParseGraphQLServer', () => { playgroundPath: '/playground', subscriptionsPath: '/subscriptions', }); + + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); }); describe('constructor', () => { @@ -3488,8 +3493,6 @@ describe('ParseGraphQLServer', () => { }); it('should require master key to create a new class', async () => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); try { await apolloClient.mutate({ @@ -3862,8 +3865,6 @@ describe('ParseGraphQLServer', () => { handleError(e); } - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); try { await apolloClient.mutate({ @@ -4091,8 +4092,6 @@ describe('ParseGraphQLServer', () => { handleError(e); } - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); try { await apolloClient.mutate({ @@ -4136,8 +4135,6 @@ describe('ParseGraphQLServer', () => { }); it('should require master key to get an existing class', async () => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); try { await apolloClient.query({ @@ -4158,8 +4155,6 @@ describe('ParseGraphQLServer', () => { }); it('should require master key to find the existing classes', async () => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); try { await apolloClient.query({ @@ -7801,8 +7796,6 @@ describe('ParseGraphQLServer', () => { }); it('should fail due to empty session token', async () => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); try { await apolloClient.query({ query: gql` @@ -7835,9 +7828,6 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - try { await apolloClient.query({ query: gql` diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 4763643d79..0380589057 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -39,6 +39,12 @@ describe('allowExpiredAuthDataToken option', () => { }); describe('Parse.User testing', () => { + let loggerErrorSpy; + beforeEach(() => { + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + }); + it('user sign up class method', async done => { const user = await Parse.User.signUp('asdf', 'zxcv'); ok(user.getSessionToken()); @@ -2633,9 +2639,6 @@ describe('Parse.User testing', () => { }); it('cannot delete session if no sessionToken', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - Promise.resolve() .then(() => { return Parse.User.signUp('test1', 'test', { foo: 'bar' }); @@ -4292,6 +4295,12 @@ describe('Security Advisory GHSA-8w3j-g983-8jh5', function () { }); describe('login as other user', () => { + let loggerErrorSpy; + beforeEach(() => { + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + }); + it('allows creating a session for another user with the master key', async done => { await Parse.User.signUp('some_user', 'some_password'); const userId = Parse.User.current().id; @@ -4387,9 +4396,6 @@ describe('login as other user', () => { }); it('rejects creating a session for another user without the master key', async done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - await Parse.User.signUp('some_user', 'some_password'); const userId = Parse.User.current().id; await Parse.User.logOut(); diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 40b9bf769c..03c68276f8 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -20,8 +20,12 @@ const hasAllPODobject = () => { }; describe('SchemaController', () => { + let loggerErrorSpy; + beforeEach(() => { config = Config.get('test'); + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); }); it('can validate one object', done => { @@ -249,9 +253,6 @@ describe('SchemaController', () => { }); it('class-level permissions test count', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - let obj; return ( config.database @@ -1433,8 +1434,12 @@ describe('SchemaController', () => { }); describe('Class Level Permissions for requiredAuth', () => { + let loggerErrorSpy; + beforeEach(() => { config = Config.get('test'); + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); }); function createUser() { @@ -1445,9 +1450,6 @@ describe('Class Level Permissions for requiredAuth', () => { } it('required auth test find', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - config.database .loadSchema() .then(schema => { @@ -1548,8 +1550,6 @@ describe('Class Level Permissions for requiredAuth', () => { }); it('required auth should reject create when not authenticated', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); config.database .loadSchema() .then(schema => { @@ -1634,9 +1634,6 @@ describe('Class Level Permissions for requiredAuth', () => { }); it('required auth test get not authenticated', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - config.database .loadSchema() .then(schema => { @@ -1676,9 +1673,6 @@ describe('Class Level Permissions for requiredAuth', () => { }); it('required auth test find not authenticated', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - config.database .loadSchema() .then(schema => { diff --git a/spec/rest.spec.js b/spec/rest.spec.js index cd0dddd624..4d8f40a982 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -11,9 +11,14 @@ let config; let database; describe('rest create', () => { + let loggerErrorSpy; + beforeEach(() => { config = Config.get('test'); database = config.database; + + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); }); it('handles _id', done => { @@ -314,9 +319,6 @@ describe('rest create', () => { }); it('handles create on non-existent class when disabled client class creation', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - const customConfig = Object.assign({}, config, { allowClientClassCreation: false, }); @@ -775,8 +777,6 @@ describe('rest create', () => { }); it('cannot get object in volatileClasses if not masterKey through pointer', async () => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); const masterKeyOnlyClassObject = new Parse.Object('_PushStatus'); await masterKeyOnlyClassObject.save(null, { useMasterKey: true }); @@ -795,8 +795,6 @@ describe('rest create', () => { }); it_id('3ce563bf-93aa-4d0b-9af9-c5fb246ac9fc')(it)('cannot get object in _GlobalConfig if not masterKey through pointer', async () => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); await Parse.Config.save({ privateData: 'secret' }, { privateData: true }); const obj2 = new Parse.Object('TestObject'); @@ -956,9 +954,15 @@ describe('rest update', () => { }); describe('read-only masterKey', () => { + let loggerErrorSpy; + let logger; + + beforeEach(() => { + logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + }); + it('properly throws on rest.create, rest.update and rest.del', () => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); const config = Config.get('test'); const readOnly = auth.readOnly(config); @@ -983,9 +987,9 @@ describe('read-only masterKey', () => { await reconfigureServer({ readOnlyMasterKey: 'yolo-read-only', }); - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - loggerErrorSpy.calls.reset(); + // Need to be re required because reconfigureServer resets the logger + const logger2 = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger2, 'error').and.callThrough(); try { await request({ url: `${Parse.serverURL}/classes/MyYolo`, @@ -1031,8 +1035,6 @@ describe('read-only masterKey', () => { }); it('should throw when trying to create RestWrite', () => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); const config = Config.get('test'); expect(() => { @@ -1044,8 +1046,6 @@ describe('read-only masterKey', () => { }); it('should throw when trying to create schema', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); request({ method: 'POST', @@ -1067,8 +1067,6 @@ describe('read-only masterKey', () => { }); it('should throw when trying to create schema with a name', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, @@ -1090,8 +1088,6 @@ describe('read-only masterKey', () => { }); it('should throw when trying to update schema', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, @@ -1113,8 +1109,6 @@ describe('read-only masterKey', () => { }); it('should throw when trying to delete schema', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, @@ -1136,8 +1130,6 @@ describe('read-only masterKey', () => { }); it('should throw when trying to update the global config', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/config`, @@ -1159,8 +1151,6 @@ describe('read-only masterKey', () => { }); it('should throw when trying to send push', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/push`, diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 3484f207fb..5d92ef36e1 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -147,9 +147,14 @@ const masterKeyHeaders = { }; describe('schemas', () => { + let loggerErrorSpy; + beforeEach(async () => { await reconfigureServer(); config = Config.get('test'); + + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); }); it('requires the master key to get all schemas', done => { @@ -167,8 +172,6 @@ describe('schemas', () => { }); it('requires the master key to get one schema', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); request({ url: 'http://localhost:8378/1/schemas/SomeSchema', @@ -183,8 +186,6 @@ describe('schemas', () => { }); it('asks for the master key if you use the rest key', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); loggerErrorSpy.calls.reset(); request({ url: 'http://localhost:8378/1/schemas', @@ -1815,9 +1816,6 @@ describe('schemas', () => { }); it('should not be able to add a field', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - request({ method: 'POST', url: 'http://localhost:8378/1/schemas/AClass', @@ -2181,9 +2179,6 @@ describe('schemas', () => { } it('validate CLP 1', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - const user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); @@ -2247,9 +2242,6 @@ describe('schemas', () => { }); it('validate CLP 2', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - const user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); @@ -2338,9 +2330,6 @@ describe('schemas', () => { }); it('validate CLP 3', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - const user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); @@ -2420,9 +2409,6 @@ describe('schemas', () => { }); it('validate CLP 4', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - const user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); @@ -2520,9 +2506,6 @@ describe('schemas', () => { }); it('validate CLP 5', done => { - const logger = require('../lib/logger').default; - const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); - const user = new Parse.User(); user.setUsername('user'); user.setPassword('user');