Skip to content

Commit 4752197

Browse files
authored
feat: Add Parse Server option enableSanitizedErrorResponse to remove detailed error messages from responses sent to clients (#9944)
1 parent 73e7812 commit 4752197

24 files changed

+121
-49
lines changed

spec/ParseFile.spec.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -767,13 +767,11 @@ describe('Parse.File testing', () => {
767767

768768
describe('getting files', () => {
769769
it('does not crash on file request with invalid app ID', async () => {
770-
loggerErrorSpy.calls.reset();
771770
const res1 = await request({
772771
url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt',
773772
}).catch(e => e);
774773
expect(res1.status).toBe(403);
775-
expect(res1.data).toEqual({ code: 119, error: 'Permission denied' });
776-
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid application ID.'));
774+
expect(res1.data).toEqual({ code: 119, error: 'Invalid application ID.' });
777775
// Ensure server did not crash
778776
const res2 = await request({ url: 'http://localhost:8378/1/health' });
779777
expect(res2.status).toEqual(200);

spec/Utils.spec.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
const Utils = require('../src/Utils');
1+
const Utils = require('../lib/Utils');
2+
const { createSanitizedError, createSanitizedHttpError } = require("../lib/Error")
23

34
describe('Utils', () => {
45
describe('encodeForUrl', () => {
@@ -173,4 +174,42 @@ describe('Utils', () => {
173174
expect(Utils.getNestedProperty(obj, 'database.name')).toBe('');
174175
});
175176
});
177+
178+
describe('createSanitizedError', () => {
179+
it('should return "Permission denied" when enableSanitizedErrorResponse is true', () => {
180+
const config = { enableSanitizedErrorResponse: true };
181+
const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', config);
182+
expect(error.message).toBe('Permission denied');
183+
});
184+
185+
it('should not crash with config undefined', () => {
186+
const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', undefined);
187+
expect(error.message).toBe('Permission denied');
188+
});
189+
190+
it('should return the detailed message when enableSanitizedErrorResponse is false', () => {
191+
const config = { enableSanitizedErrorResponse: false };
192+
const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', config);
193+
expect(error.message).toBe('Detailed error message');
194+
});
195+
});
196+
197+
describe('createSanitizedHttpError', () => {
198+
it('should return "Permission denied" when enableSanitizedErrorResponse is true', () => {
199+
const config = { enableSanitizedErrorResponse: true };
200+
const error = createSanitizedHttpError(403, 'Detailed error message', config);
201+
expect(error.message).toBe('Permission denied');
202+
});
203+
204+
it('should not crash with config undefined', () => {
205+
const error = createSanitizedHttpError(403, 'Detailed error message', undefined);
206+
expect(error.message).toBe('Permission denied');
207+
});
208+
209+
it('should return the detailed message when enableSanitizedErrorResponse is false', () => {
210+
const config = { enableSanitizedErrorResponse: false };
211+
const error = createSanitizedHttpError(403, 'Detailed error message', config);
212+
expect(error.message).toBe('Detailed error message');
213+
});
214+
});
176215
});

src/Controllers/SchemaController.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,19 +1399,22 @@ export default class SchemaController {
13991399
return true;
14001400
}
14011401
const perms = classPermissions[operation];
1402+
const config = Config.get(Parse.applicationId)
14021403
// If only for authenticated users
14031404
// make sure we have an aclGroup
14041405
if (perms['requiresAuthentication']) {
14051406
// If aclGroup has * (public)
14061407
if (!aclGroup || aclGroup.length == 0) {
14071408
throw createSanitizedError(
14081409
Parse.Error.OBJECT_NOT_FOUND,
1409-
'Permission denied, user needs to be authenticated.'
1410+
'Permission denied, user needs to be authenticated.',
1411+
config
14101412
);
14111413
} else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) {
14121414
throw createSanitizedError(
14131415
Parse.Error.OBJECT_NOT_FOUND,
1414-
'Permission denied, user needs to be authenticated.'
1416+
'Permission denied, user needs to be authenticated.',
1417+
config
14151418
);
14161419
}
14171420
// requiresAuthentication passed, just move forward
@@ -1428,7 +1431,8 @@ export default class SchemaController {
14281431
if (permissionField == 'writeUserFields' && operation == 'create') {
14291432
throw createSanitizedError(
14301433
Parse.Error.OPERATION_FORBIDDEN,
1431-
`Permission denied for action ${operation} on class ${className}.`
1434+
`Permission denied for action ${operation} on class ${className}.`,
1435+
config
14321436
);
14331437
}
14341438

@@ -1451,7 +1455,8 @@ export default class SchemaController {
14511455

14521456
throw createSanitizedError(
14531457
Parse.Error.OPERATION_FORBIDDEN,
1454-
`Permission denied for action ${operation} on class ${className}.`
1458+
`Permission denied for action ${operation} on class ${className}.`,
1459+
config
14551460
);
14561461
}
14571462

src/Error.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import defaultLogger from './logger';
88
* @param {string} detailedMessage - The detailed error message to log server-side
99
* @returns {Parse.Error} A Parse.Error with sanitized message
1010
*/
11-
function createSanitizedError(errorCode, detailedMessage) {
11+
function createSanitizedError(errorCode, detailedMessage, config) {
1212
// On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file
1313
if (process.env.TESTING) {
1414
defaultLogger.error('Sanitized error:', detailedMessage);
1515
} else {
1616
defaultLogger.error(detailedMessage);
1717
}
1818

19-
return new Parse.Error(errorCode, 'Permission denied');
19+
return new Parse.Error(errorCode, config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage);
2020
}
2121

2222
/**
@@ -27,7 +27,7 @@ function createSanitizedError(errorCode, detailedMessage) {
2727
* @param {string} detailedMessage - The detailed error message to log server-side
2828
* @returns {Error} An Error with sanitized message
2929
*/
30-
function createSanitizedHttpError(statusCode, detailedMessage) {
30+
function createSanitizedHttpError(statusCode, detailedMessage, config) {
3131
// On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file
3232
if (process.env.TESTING) {
3333
defaultLogger.error('Sanitized error:', detailedMessage);
@@ -37,7 +37,7 @@ function createSanitizedHttpError(statusCode, detailedMessage) {
3737

3838
const error = new Error();
3939
error.status = statusCode;
40-
error.message = 'Permission denied';
40+
error.message = config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage;
4141
return error;
4242
}
4343

src/GraphQL/loaders/schemaMutations.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ const load = parseGraphQLSchema => {
3131
const { name, schemaFields } = deepcopy(args);
3232
const { config, auth } = context;
3333

34-
enforceMasterKeyAccess(auth);
34+
enforceMasterKeyAccess(auth, config);
3535

3636
if (auth.isReadOnly) {
3737
throw createSanitizedError(
3838
Parse.Error.OPERATION_FORBIDDEN,
3939
"read-only masterKey isn't allowed to create a schema.",
40+
config
4041
);
4142
}
4243

@@ -80,12 +81,13 @@ const load = parseGraphQLSchema => {
8081
const { name, schemaFields } = deepcopy(args);
8182
const { config, auth } = context;
8283

83-
enforceMasterKeyAccess(auth);
84+
enforceMasterKeyAccess(auth, config);
8485

8586
if (auth.isReadOnly) {
8687
throw createSanitizedError(
8788
Parse.Error.OPERATION_FORBIDDEN,
88-
"read-only masterKey isn't allowed to update a schema."
89+
"read-only masterKey isn't allowed to update a schema.",
90+
config
8991
);
9092
}
9193

@@ -131,12 +133,13 @@ const load = parseGraphQLSchema => {
131133
const { name } = deepcopy(args);
132134
const { config, auth } = context;
133135

134-
enforceMasterKeyAccess(auth);
136+
enforceMasterKeyAccess(auth, config);
135137

136138
if (auth.isReadOnly) {
137139
throw createSanitizedError(
138140
Parse.Error.OPERATION_FORBIDDEN,
139141
"read-only masterKey isn't allowed to delete a schema.",
142+
config
140143
);
141144
}
142145

src/GraphQL/loaders/schemaQueries.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const load = parseGraphQLSchema => {
3131
const { name } = deepcopy(args);
3232
const { config, auth } = context;
3333

34-
enforceMasterKeyAccess(auth);
34+
enforceMasterKeyAccess(auth, config);
3535

3636
const schema = await config.database.loadSchema({ clearCache: true });
3737
const parseClass = await getClass(name, schema);
@@ -57,7 +57,7 @@ const load = parseGraphQLSchema => {
5757
try {
5858
const { config, auth } = context;
5959

60-
enforceMasterKeyAccess(auth);
60+
enforceMasterKeyAccess(auth, config);
6161

6262
const schema = await config.database.loadSchema({ clearCache: true });
6363
return (await schema.getAllClasses(true)).map(parseClass => ({

src/GraphQL/loaders/usersQueries.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { createSanitizedError } from '../../Error';
99
const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) => {
1010
const { info, config } = context;
1111
if (!info || !info.sessionToken) {
12-
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
12+
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
1313
}
1414
const sessionToken = info.sessionToken;
1515
const selectedFields = getFieldNames(queryInfo)
@@ -63,7 +63,7 @@ const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) =
6363
info.context
6464
);
6565
if (!response.results || response.results.length == 0) {
66-
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
66+
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
6767
} else {
6868
const user = response.results[0];
6969
return {

src/GraphQL/parseGraphQLUtils.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import Parse from 'parse/node';
22
import { GraphQLError } from 'graphql';
33
import { createSanitizedError } from '../Error';
44

5-
export function enforceMasterKeyAccess(auth) {
5+
export function enforceMasterKeyAccess(auth, config) {
66
if (!auth.isMaster) {
77
throw createSanitizedError(
88
Parse.Error.OPERATION_FORBIDDEN,
99
'unauthorized: master key is required',
10+
config
1011
);
1112
}
1213
}

src/Options/Definitions.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,13 @@ module.exports.ParseServerOptions = {
247247
action: parsers.booleanParser,
248248
default: true,
249249
},
250+
enableSanitizedErrorResponse: {
251+
env: 'PARSE_SERVER_ENABLE_SANITIZED_ERROR_RESPONSE',
252+
help:
253+
'If set to `true`, error details are removed from error messages in responses to client requests, and instead a generic error message is sent. Default is `true`.',
254+
action: parsers.booleanParser,
255+
default: true,
256+
},
250257
encodeParseObjectInCloudFunction: {
251258
env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION',
252259
help:

src/Options/docs.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)