diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js index 1525147a40..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,55 +270,65 @@ describe('AudiencesRouter', () => { }); it('should only create with master key', done => { + loggerErrorSpy.calls.reset(); Parse._request('POST', 'push_audiences', { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' }), }).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); }); it('should only find with master key', done => { + loggerErrorSpy.calls.reset(); Parse._request('GET', 'push_audiences', {}).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); }); it('should only get with master key', done => { + loggerErrorSpy.calls.reset(); Parse._request('GET', `push_audiences/someId`, {}).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); }); it('should only update with master key', done => { + loggerErrorSpy.calls.reset(); Parse._request('PUT', `push_audiences/someId`, { name: 'My Audience 2', }).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); } ); }); it('should only delete with master key', done => { + loggerErrorSpy.calls.reset(); Parse._request('DELETE', `push_audiences/someId`, {}).then( () => {}, error => { - expect(error.message).toEqual('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); + 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 b25ac25be5..d4b77baaa8 100644 --- a/spec/LogsRouter.spec.js +++ b/spec/LogsRouter.spec.js @@ -52,6 +52,9 @@ describe_only(() => { }); it('can check invalid master key of request', done => { + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + loggerErrorSpy.calls.reset(); request({ url: 'http://localhost:8378/1/scriptlog', headers: { @@ -61,7 +64,8 @@ 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'); + 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 6edfa79109..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 TestUtils = require('../lib/TestUtils'); +const { 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,15 @@ describe('miscellaneous', () => { }); it('fail on purge all objects in class without master key', done => { + 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', }; + loggerErrorSpy.calls.reset(); request({ method: 'DELETE', headers: headers, @@ -1724,7 +1728,8 @@ 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'); + 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 d6539b7336..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 = { @@ -146,6 +153,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 + loggerErrorSpy.calls.reset(); request({ method: 'DELETE', headers: { @@ -156,8 +164,10 @@ 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); // incorrect X-Parse-Master-Key header + loggerErrorSpy.calls.reset(); request({ method: 'DELETE', headers: { @@ -169,7 +179,8 @@ 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); done(); }); }); @@ -756,11 +767,13 @@ describe('Parse.File testing', () => { describe('getting files', () => { it('does not crash on file request with invalid app ID', async () => { + 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: 'Invalid application ID.' }); + expect(res1.data).toEqual({ code: 119, error: 'Permission denied' }); + 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 e6719433ff..1b3a9adc0d 100644 --- a/spec/ParseGlobalConfig.spec.js +++ b/spec/ParseGlobalConfig.spec.js @@ -220,6 +220,9 @@ describe('a GlobalConfig', () => { }); it('fail to update if master key is missing', done => { + 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', @@ -233,7 +236,8 @@ 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'); + 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 5031a9cdff..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,6 +3493,7 @@ describe('ParseGraphQLServer', () => { }); it('should require master key to create a new class', async () => { + loggerErrorSpy.calls.reset(); try { await apolloClient.mutate({ mutation: gql` @@ -3501,7 +3507,8 @@ 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); @@ -3858,6 +3865,7 @@ describe('ParseGraphQLServer', () => { handleError(e); } + loggerErrorSpy.calls.reset(); try { await apolloClient.mutate({ mutation: gql` @@ -3871,7 +3879,8 @@ 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); @@ -4083,6 +4092,7 @@ describe('ParseGraphQLServer', () => { handleError(e); } + loggerErrorSpy.calls.reset(); try { await apolloClient.mutate({ mutation: gql` @@ -4096,7 +4106,8 @@ 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); @@ -4124,6 +4135,7 @@ describe('ParseGraphQLServer', () => { }); it('should require master key to get an existing class', async () => { + loggerErrorSpy.calls.reset(); try { await apolloClient.query({ query: gql` @@ -4137,11 +4149,13 @@ 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); it('should require master key to find the existing classes', async () => { + loggerErrorSpy.calls.reset(); try { await apolloClient.query({ query: gql` @@ -4155,7 +4169,8 @@ 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required')); } }); }); @@ -6081,7 +6096,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 +6130,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', { @@ -7802,7 +7817,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')); } }); @@ -7840,7 +7856,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 c03a727b4a..408e8fa7bf 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -157,6 +157,9 @@ describe('Installations', () => { }); it('should properly fail queying installations', done => { + const logger = require('../lib/logger').default; + const loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'android'; const input = { @@ -166,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(); }) @@ -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( - "Clients aren't allowed to perform the find operation on the installation collection." + 'Permission denied' ); + 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 d75658b19e..eb9c03ac4e 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -74,10 +74,14 @@ describe('Parse.Query Aggregate testing', () => { }); it('should only query aggregate with master key', done => { + 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('unauthorized: master key is required'); + expect(error.message).toEqual('Permission denied'); + 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 ba34fbf6e9..0380589057 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -13,6 +13,7 @@ const passwordCrypto = require('../lib/password'); const Config = require('../lib/Config'); const cryptoUtils = require('../lib/cryptoUtils'); + describe('allowExpiredAuthDataToken option', () => { it('should accept true value', async () => { await reconfigureServer({ allowExpiredAuthDataToken: true }); @@ -38,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()); @@ -2651,6 +2658,7 @@ describe('Parse.User testing', () => { const b = response.data; expect(b.results.length).toEqual(1); const objId = b.results[0].objectId; + loggerErrorSpy.calls.reset(); request({ method: 'DELETE', headers: { @@ -2661,7 +2669,9 @@ 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'); + + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid session token')); done(); }); }); @@ -3355,6 +3365,9 @@ describe('Parse.User testing', () => { sendMail: () => Promise.resolve(), }; + let logger; + let loggerErrorSpy; + const user = new Parse.User(); user.set({ username: 'hello', @@ -3369,9 +3382,12 @@ describe('Parse.User testing', () => { publicServerURL: 'http://localhost:8378/1', }) .then(() => { + logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); return user.signUp(); }) .then(() => { + loggerErrorSpy.calls.reset(); return Parse.User.current().set('emailVerified', true).save(); }) .then(() => { @@ -3379,7 +3395,9 @@ 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to manually update email verification.")); + done(); }); }); @@ -4277,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; @@ -4376,6 +4400,7 @@ describe('login as other user', () => { const userId = Parse.User.current().id; await Parse.User.logOut(); + loggerErrorSpy.calls.reset(); try { await request({ method: 'POST', @@ -4393,7 +4418,8 @@ 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'); + 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 7b676da1ea..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 querystring = require('querystring'); let config; @@ -155,9 +154,13 @@ describe('rest query', () => { }); it('query 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, }); + loggerErrorSpy.calls.reset(); rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}).then( () => { fail('Should throw an error'); @@ -165,9 +168,8 @@ 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('This user is not allowed to access ' + 'non-existent class: ClientClassCreation')); done(); } ); @@ -243,7 +245,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..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 => { @@ -275,6 +279,7 @@ describe('SchemaController', () => { }) .then(results => { expect(results.length).toBe(1); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('Stuff'); return query.count(); }) @@ -283,7 +288,9 @@ 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'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action count on class Stuff')); done(); } ) @@ -1427,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() { @@ -1453,6 +1464,7 @@ describe('Class Level Permissions for requiredAuth', () => { }); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('Stuff'); return query.find(); }) @@ -1462,7 +1474,8 @@ 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.')); done(); } ); @@ -1551,6 +1564,7 @@ describe('Class Level Permissions for requiredAuth', () => { }); }) .then(() => { + loggerErrorSpy.calls.reset(); const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff.save(); @@ -1561,7 +1575,8 @@ 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.')); done(); } ); @@ -1639,6 +1654,7 @@ describe('Class Level Permissions for requiredAuth', () => { const stuff = new Parse.Object('Stuff'); stuff.set('foo', 'bar'); return stuff.save().then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('Stuff'); return query.get(stuff.id); }); @@ -1649,7 +1665,8 @@ 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied, user needs to be authenticated.')); done(); } ); @@ -1685,6 +1702,7 @@ describe('Class Level Permissions for requiredAuth', () => { }) .then(result => { expect(result.get('foo')).toEqual('bar'); + loggerErrorSpy.calls.reset(); const query = new Parse.Query('Stuff'); return query.find(); }) @@ -1694,7 +1712,8 @@ 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'); + 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 f138fe4cf6..201e01293d 100644 --- a/spec/features.spec.js +++ b/spec/features.spec.js @@ -20,6 +20,9 @@ describe('features', () => { }); it('requires the master key to get features', async done => { + 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', @@ -32,7 +35,8 @@ 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'); + 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 1fff4fad59..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 => { @@ -317,6 +322,7 @@ describe('rest create', () => { const customConfig = Object.assign({}, config, { allowClientClassCreation: false, }); + loggerErrorSpy.calls.reset(); rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}).then( () => { fail('Should throw an error'); @@ -324,9 +330,8 @@ 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('This user is not allowed to access ' + 'non-existent class: ClientClassCreation')); done(); } ); @@ -772,6 +777,7 @@ describe('rest create', () => { }); it('cannot get object in volatileClasses if not masterKey through pointer', async () => { + loggerErrorSpy.calls.reset(); const masterKeyOnlyClassObject = new Parse.Object('_PushStatus'); await masterKeyOnlyClassObject.save(null, { useMasterKey: true }); const obj2 = new Parse.Object('TestObject'); @@ -783,11 +789,13 @@ 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' ); + 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 () => { + loggerErrorSpy.calls.reset(); await Parse.Config.save({ privateData: 'secret' }, { privateData: true }); const obj2 = new Parse.Object('TestObject'); obj2.set('globalConfigPointer', { @@ -799,8 +807,9 @@ 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' ); + 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 => { @@ -945,7 +954,16 @@ 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', () => { + loggerErrorSpy.calls.reset(); const config = Config.get('test'); const readOnly = auth.readOnly(config); expect(() => { @@ -953,9 +971,10 @@ 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(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to perform the create operation.")); expect(() => { rest.update(config, readOnly, 'AnObject', {}); }).toThrow(); @@ -968,6 +987,9 @@ describe('read-only masterKey', () => { await reconfigureServer({ readOnlyMasterKey: 'yolo-read-only', }); + // 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`, @@ -983,8 +1005,9 @@ 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' ); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to perform the create operation.")); } await reconfigureServer(); }); @@ -1012,18 +1035,18 @@ describe('read-only masterKey', () => { }); it('should throw when trying to create RestWrite', () => { + loggerErrorSpy.calls.reset(); 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') ); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Cannot perform a write operation when using readOnlyMasterKey")); }); it('should throw when trying to create schema', done => { + loggerErrorSpy.calls.reset(); request({ method: 'POST', url: `${Parse.serverURL}/schemas`, @@ -1037,12 +1060,14 @@ 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'); + 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 => { + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'POST', @@ -1056,12 +1081,14 @@ 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'); + 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 => { + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'PUT', @@ -1075,12 +1102,14 @@ 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'); + 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 => { + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'DELETE', @@ -1094,12 +1123,14 @@ 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'); + 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 => { + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/config`, method: 'PUT', @@ -1113,12 +1144,14 @@ 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'); + 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 => { + loggerErrorSpy.calls.reset(); request({ url: `${Parse.serverURL}/push`, method: 'POST', @@ -1133,8 +1166,9 @@ 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' ); + 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 7891fa847e..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,25 +172,29 @@ describe('schemas', () => { }); it('requires the master key to get one schema', done => { + loggerErrorSpy.calls.reset(); request({ url: 'http://localhost:8378/1/schemas/SomeSchema', json: true, 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'); + 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 => { + loggerErrorSpy.calls.reset(); request({ url: 'http://localhost:8378/1/schemas', json: true, 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'); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("unauthorized: master key is required")); done(); }); }); @@ -1826,6 +1835,7 @@ describe('schemas', () => { }, }, }).then(() => { + loggerErrorSpy.calls.reset(); const object = new Parse.Object('AClass'); object.set('hello', 'world'); return object.save().then( @@ -1834,7 +1844,9 @@ describe('schemas', () => { done(); }, err => { - expect(err.message).toEqual('Permission denied for action addField on class AClass.'); + expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action addField on class AClass')); done(); } ); @@ -2198,13 +2210,16 @@ describe('schemas', () => { }); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { 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'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2258,13 +2273,16 @@ describe('schemas', () => { }); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { 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'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2343,13 +2361,16 @@ describe('schemas', () => { }); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { 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'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2419,13 +2440,16 @@ describe('schemas', () => { }); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { 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'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2450,13 +2474,16 @@ describe('schemas', () => { ); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find().then( () => { 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'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action find on class AClass')); return Promise.resolve(); } ); @@ -2531,6 +2558,7 @@ describe('schemas', () => { return Parse.User.logIn('admin', 'admin'); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find(); }) @@ -2540,7 +2568,9 @@ describe('schemas', () => { return Promise.resolve(); }, err => { - expect(err.message).toEqual('Permission denied for action create on class AClass.'); + expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Permission denied for action create on class AClass')); return Promise.resolve(); } ) @@ -2548,6 +2578,7 @@ describe('schemas', () => { return Parse.User.logIn('user2', 'user2'); }) .then(() => { + loggerErrorSpy.calls.reset(); const query = new Parse.Query('AClass'); return query.find(); }) @@ -2557,7 +2588,9 @@ describe('schemas', () => { return Promise.resolve(); }, err => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); + expect(err.message).toEqual('Permission denied'); + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + 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 0d66c0a135..f3aff6ef0b 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -13,9 +13,13 @@ describe('Vulnerabilities', () => { }); it('denies user creation with poisoned object ID', async () => { + 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, 'Invalid object ID.')); + ).toBeRejectedWith(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')); + expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Invalid object ID.")); }); describe('existing sessions for users with poisoned object ID', () => { diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index fccadd23ce..b21d4b3318 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 '../Error'; +import defaultLogger from '../logger'; // @flow-disable-next import deepcopy from 'deepcopy'; import type { @@ -1403,12 +1405,12 @@ export default class SchemaController { if (perms['requiresAuthentication']) { // If aclGroup has * (public) if (!aclGroup || aclGroup.length == 0) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OBJECT_NOT_FOUND, 'Permission denied, user needs to be authenticated.' ); } else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OBJECT_NOT_FOUND, 'Permission denied, user needs to be authenticated.' ); @@ -1425,7 +1427,7 @@ export default class SchemaController { // Reject create when write lockdown if (permissionField == 'writeUserFields' && operation == 'create') { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, `Permission denied for action ${operation} on class ${className}.` ); @@ -1448,7 +1450,7 @@ export default class SchemaController { } } - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, `Permission denied for action ${operation} on class ${className}.` ); diff --git a/src/Error.js b/src/Error.js new file mode 100644 index 0000000000..4729965e1c --- /dev/null +++ b/src/Error.js @@ -0,0 +1,44 @@ +import defaultLogger from './logger'; + +/** + * 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 + * @returns {Parse.Error} A Parse.Error with sanitized message + */ +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); + } else { + defaultLogger.error(detailedMessage); + } + + return new Parse.Error(errorCode, 'Permission denied'); +} + +/** + * 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 + * @returns {Error} An Error with sanitized message + */ +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); + } else { + defaultLogger.error(detailedMessage); + } + + const error = new Error(); + error.status = statusCode; + error.message = 'Permission denied'; + return error; +} + +export { createSanitizedError, createSanitizedHttpError }; diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index ffb4d6523b..5dd8969bd9 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -6,6 +6,7 @@ import * as schemaTypes from './schemaTypes'; import { transformToParse, transformToGraphQL } from '../transformers/schemaFields'; import { enforceMasterKeyAccess } from '../parseGraphQLUtils'; import { getClass } from './schemaQueries'; +import { createSanitizedError } from '../../Error'; const load = parseGraphQLSchema => { const createClassMutation = mutationWithClientMutationId({ @@ -33,9 +34,9 @@ const load = parseGraphQLSchema => { enforceMasterKeyAccess(auth); if (auth.isReadOnly) { - throw new Parse.Error( + 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.", ); } @@ -82,7 +83,7 @@ const load = parseGraphQLSchema => { enforceMasterKeyAccess(auth); if (auth.isReadOnly) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to update a schema." ); @@ -133,9 +134,9 @@ const load = parseGraphQLSchema => { enforceMasterKeyAccess(auth); if (auth.isReadOnly) { - throw new Parse.Error( + 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.", ); } 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/GraphQL/parseGraphQLUtils.js b/src/GraphQL/parseGraphQLUtils.js index f1194784cb..1a0b266a36 100644 --- a/src/GraphQL/parseGraphQLUtils.js +++ b/src/GraphQL/parseGraphQLUtils.js @@ -1,9 +1,13 @@ import Parse from 'parse/node'; import { GraphQLError } from 'graphql'; +import { createSanitizedError } from '../Error'; export function enforceMasterKeyAccess(auth) { if (!auth.isMaster) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'unauthorized: master key is required'); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + 'unauthorized: master key is required', + ); } } diff --git a/src/RestQuery.js b/src/RestQuery.js index c48cecdb6f..ba3d05ddeb 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -7,6 +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('./Error'); // restOptions can include: // skip @@ -51,7 +52,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 +121,7 @@ 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'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } this.restWhere = { $and: [ @@ -421,7 +422,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,7 +801,7 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () { ) || []; for (const key of protectedFields) { if (this.restWhere[key]) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, `This user is not allowed to query ${key} on class ${this.className}` ); diff --git a/src/RestWrite.js b/src/RestWrite.js index 41b6c23468..c8c9584fde 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -17,6 +17,7 @@ import RestQuery from './RestQuery'; import _ from 'lodash'; import logger from './logger'; import { requiredColumns } from './Controllers/SchemaController'; +import { createSanitizedError } from './Error'; // query and data are both provided in REST API format. So data // types are encoded by plain old objects. @@ -29,9 +30,9 @@ import { requiredColumns } from './Controllers/SchemaController'; // 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; @@ -199,9 +200,9 @@ RestWrite.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 + 'This user is not allowed to access non-existent class: ' + this.className, ); } }); @@ -566,7 +567,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( @@ -660,8 +660,10 @@ 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,7 +1452,7 @@ RestWrite.prototype.runDatabaseOperation = function () { } if (this.className === '_User' && this.query && this.auth.isUnauthenticated()) { - throw new Parse.Error( + throw createSanitizedError( Parse.Error.SESSION_MISSING, `Cannot modify user ${this.query.objectId}.` ); diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 8b6e447757..ae774705d6 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 '../Error'; +import defaultLogger from '../logger'; const ALLOWED_GET_QUERY_KEYS = [ 'keys', @@ -111,7 +113,7 @@ 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.'); + 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 5cb39abf47..12c523c3b0 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -5,6 +5,7 @@ import Config from '../Config'; import logger from '../logger'; const triggers = require('../triggers'); const Utils = require('../Utils'); +import { createSanitizedError } from '../Error'; export class FilesRouter { expressRouter({ maxUploadSize = '20Mb' } = {}) { @@ -43,7 +44,7 @@ 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 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 5a28b3bae1..4b107ee878 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -3,6 +3,7 @@ import Parse from 'parse/node'; import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import * as triggers from '../triggers'; +import { createSanitizedError } from '../Error'; const getConfigFromParams = params => { const config = new Parse.Config(); @@ -41,9 +42,9 @@ export class GlobalConfigRouter extends PromiseRouter { async updateGlobalConfig(req) { if (req.auth.isReadOnly) { - throw new Parse.Error( + 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.", ); } const params = req.body.params || {}; diff --git a/src/Routers/GraphQLRouter.js b/src/Routers/GraphQLRouter.js index d472ac9df5..d785afbdf2 100644 --- a/src/Routers/GraphQLRouter.js +++ b/src/Routers/GraphQLRouter.js @@ -1,6 +1,7 @@ import Parse from 'parse/node'; import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; +import { createSanitizedError } from '../Error'; const GraphQLConfigPath = '/graphql-config'; @@ -14,9 +15,9 @@ export class GraphQLRouter extends PromiseRouter { async updateGraphQLConfig(req) { if (req.auth.isReadOnly) { - throw new Parse.Error( + 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.", ); } const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {}); diff --git a/src/Routers/PurgeRouter.js b/src/Routers/PurgeRouter.js index 3195d134af..7b992a48e2 100644 --- a/src/Routers/PurgeRouter.js +++ b/src/Routers/PurgeRouter.js @@ -1,13 +1,14 @@ import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import Parse from 'parse/node'; +import { createSanitizedError } from '../Error'; export class PurgeRouter extends PromiseRouter { handlePurge(req) { if (req.auth.isReadOnly) { - throw new Parse.Error( + 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.", ); } return req.config.database diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js index 1c1c8f3b5f..123677f138 100644 --- a/src/Routers/PushRouter.js +++ b/src/Routers/PushRouter.js @@ -1,6 +1,7 @@ import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; import { Parse } from 'parse/node'; +import { createSanitizedError } from '../Error'; export class PushRouter extends PromiseRouter { mountRoutes() { @@ -9,9 +10,9 @@ export class PushRouter extends PromiseRouter { static handlePOST(req) { if (req.auth.isReadOnly) { - throw new Parse.Error( + 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.", ); } const pushController = req.config.pushController; diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index 0a42123af7..ff55711a69 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -5,6 +5,7 @@ var Parse = require('parse/node').Parse, import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; +import { createSanitizedError } from '../Error'; function classNameMismatchResponse(bodyClass, pathClass) { throw new Parse.Error( @@ -72,9 +73,9 @@ export const internalUpdateSchema = async (className, body, config) => { async function createSchema(req) { checkIfDefinedSchemasIsUsed(req); if (req.auth.isReadOnly) { - throw new Parse.Error( + 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.", ); } if (req.params.className && req.body?.className) { @@ -94,9 +95,9 @@ async function createSchema(req) { function modifySchema(req) { checkIfDefinedSchemasIsUsed(req); if (req.auth.isReadOnly) { - throw new Parse.Error( + 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.", ); } if (req.body?.className && req.body.className != req.params.className) { @@ -109,9 +110,9 @@ function modifySchema(req) { const deleteSchema = req => { if (req.auth.isReadOnly) { - throw new Parse.Error( + 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.", ); } if (!SchemaController.classNameIsValid(req.params.className)) { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 70745c2c69..f50f9608d2 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -17,6 +17,7 @@ import { import { promiseEnsureIdempotency } from '../middlewares'; import RestWrite from '../RestWrite'; import { logger } from '../logger'; +import { createSanitizedError } from '../Error'; export class UsersRouter extends ClassesRouter { className() { @@ -171,7 +172,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 +187,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. @@ -334,7 +335,10 @@ export class UsersRouter extends ClassesRouter { */ async handleLogInAs(req) { if (!req.auth.isMaster) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'master key is required'); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + 'master key is required', + ); } const userId = req.body?.userId || req.query.userId; diff --git a/src/SharedRest.js b/src/SharedRest.js index 0b4a07c320..1d342b595a 100644 --- a/src/SharedRest.js +++ b/src/SharedRest.js @@ -6,12 +6,16 @@ const classesWithMasterOnlyAccess = [ '_JobSchedule', '_Idempotency', ]; +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 error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + throw createSanitizedError( + Parse.Error.OPERATION_FORBIDDEN, + `Clients aren't allowed to perform the ${method} operation on the installation collection.` + ); } } @@ -21,14 +25,18 @@ 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); + 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 error = `read-only masterKey isn't allowed to perform the ${method} operation.`; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + 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..ec4cb29554 100644 --- a/src/TestUtils.js +++ b/src/TestUtils.js @@ -81,3 +81,4 @@ export class Connections { return this.sockets.size; } } + diff --git a/src/middlewares.js b/src/middlewares.js index 93b16f3846..c5b644379e 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 './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'; @@ -501,8 +502,9 @@ 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 error = createSanitizedHttpError(403, 'unauthorized: master key is required'); + res.status(error.status); + res.end(`{"error":"${error.message}"}`); return; } next(); @@ -510,10 +512,7 @@ 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; + throw createSanitizedHttpError(403, 'unauthorized: master key is required'); } return Promise.resolve(); } diff --git a/src/rest.js b/src/rest.js index e2e688a972..66feae66f0 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; @@ -326,7 +327,7 @@ function handleSessionMissingError(error, className, auth) { !auth.isMaster && !auth.isMaintenance ) { - throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth.'); + throw createSanitizedError(Parse.Error.SESSION_MISSING, 'Insufficient auth.'); } throw error; }