feat: Add Parse Server option enableSanitizedErrorResponse to remove detailed error messages from responses sent to clients (#9944)

This commit is contained in:
Lucas Coratger
2025-11-28 19:48:35 +01:00
committed by GitHub
parent 73e78127c2
commit 47521974ae
24 changed files with 121 additions and 49 deletions

View File

@@ -767,13 +767,11 @@ describe('Parse.File testing', () => {
describe('getting files', () => { describe('getting files', () => {
it('does not crash on file request with invalid app ID', async () => { it('does not crash on file request with invalid app ID', async () => {
loggerErrorSpy.calls.reset();
const res1 = await request({ const res1 = await request({
url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt', url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt',
}).catch(e => e); }).catch(e => e);
expect(res1.status).toBe(403); expect(res1.status).toBe(403);
expect(res1.data).toEqual({ code: 119, error: 'Permission denied' }); expect(res1.data).toEqual({ code: 119, error: 'Invalid application ID.' });
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid application ID.'));
// Ensure server did not crash // Ensure server did not crash
const res2 = await request({ url: 'http://localhost:8378/1/health' }); const res2 = await request({ url: 'http://localhost:8378/1/health' });
expect(res2.status).toEqual(200); expect(res2.status).toEqual(200);

View File

@@ -1,4 +1,5 @@
const Utils = require('../src/Utils'); const Utils = require('../lib/Utils');
const { createSanitizedError, createSanitizedHttpError } = require("../lib/Error")
describe('Utils', () => { describe('Utils', () => {
describe('encodeForUrl', () => { describe('encodeForUrl', () => {
@@ -173,4 +174,42 @@ describe('Utils', () => {
expect(Utils.getNestedProperty(obj, 'database.name')).toBe(''); expect(Utils.getNestedProperty(obj, 'database.name')).toBe('');
}); });
}); });
describe('createSanitizedError', () => {
it('should return "Permission denied" when enableSanitizedErrorResponse is true', () => {
const config = { enableSanitizedErrorResponse: true };
const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', config);
expect(error.message).toBe('Permission denied');
});
it('should not crash with config undefined', () => {
const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', undefined);
expect(error.message).toBe('Permission denied');
});
it('should return the detailed message when enableSanitizedErrorResponse is false', () => {
const config = { enableSanitizedErrorResponse: false };
const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', config);
expect(error.message).toBe('Detailed error message');
});
});
describe('createSanitizedHttpError', () => {
it('should return "Permission denied" when enableSanitizedErrorResponse is true', () => {
const config = { enableSanitizedErrorResponse: true };
const error = createSanitizedHttpError(403, 'Detailed error message', config);
expect(error.message).toBe('Permission denied');
});
it('should not crash with config undefined', () => {
const error = createSanitizedHttpError(403, 'Detailed error message', undefined);
expect(error.message).toBe('Permission denied');
});
it('should return the detailed message when enableSanitizedErrorResponse is false', () => {
const config = { enableSanitizedErrorResponse: false };
const error = createSanitizedHttpError(403, 'Detailed error message', config);
expect(error.message).toBe('Detailed error message');
});
});
}); });

View File

@@ -1399,6 +1399,7 @@ export default class SchemaController {
return true; return true;
} }
const perms = classPermissions[operation]; const perms = classPermissions[operation];
const config = Config.get(Parse.applicationId)
// If only for authenticated users // If only for authenticated users
// make sure we have an aclGroup // make sure we have an aclGroup
if (perms['requiresAuthentication']) { if (perms['requiresAuthentication']) {
@@ -1406,12 +1407,14 @@ export default class SchemaController {
if (!aclGroup || aclGroup.length == 0) { if (!aclGroup || aclGroup.length == 0) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OBJECT_NOT_FOUND, Parse.Error.OBJECT_NOT_FOUND,
'Permission denied, user needs to be authenticated.' 'Permission denied, user needs to be authenticated.',
config
); );
} else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) { } else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OBJECT_NOT_FOUND, Parse.Error.OBJECT_NOT_FOUND,
'Permission denied, user needs to be authenticated.' 'Permission denied, user needs to be authenticated.',
config
); );
} }
// requiresAuthentication passed, just move forward // requiresAuthentication passed, just move forward
@@ -1428,7 +1431,8 @@ export default class SchemaController {
if (permissionField == 'writeUserFields' && operation == 'create') { if (permissionField == 'writeUserFields' && operation == 'create') {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
`Permission denied for action ${operation} on class ${className}.` `Permission denied for action ${operation} on class ${className}.`,
config
); );
} }
@@ -1451,7 +1455,8 @@ export default class SchemaController {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
`Permission denied for action ${operation} on class ${className}.` `Permission denied for action ${operation} on class ${className}.`,
config
); );
} }

View File

@@ -8,7 +8,7 @@ import defaultLogger from './logger';
* @param {string} detailedMessage - The detailed error message to log server-side * @param {string} detailedMessage - The detailed error message to log server-side
* @returns {Parse.Error} A Parse.Error with sanitized message * @returns {Parse.Error} A Parse.Error with sanitized message
*/ */
function createSanitizedError(errorCode, detailedMessage) { function createSanitizedError(errorCode, detailedMessage, config) {
// On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file // 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) { if (process.env.TESTING) {
defaultLogger.error('Sanitized error:', detailedMessage); defaultLogger.error('Sanitized error:', detailedMessage);
@@ -16,7 +16,7 @@ function createSanitizedError(errorCode, detailedMessage) {
defaultLogger.error(detailedMessage); defaultLogger.error(detailedMessage);
} }
return new Parse.Error(errorCode, 'Permission denied'); return new Parse.Error(errorCode, config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage);
} }
/** /**
@@ -27,7 +27,7 @@ function createSanitizedError(errorCode, detailedMessage) {
* @param {string} detailedMessage - The detailed error message to log server-side * @param {string} detailedMessage - The detailed error message to log server-side
* @returns {Error} An Error with sanitized message * @returns {Error} An Error with sanitized message
*/ */
function createSanitizedHttpError(statusCode, detailedMessage) { function createSanitizedHttpError(statusCode, detailedMessage, config) {
// On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file // 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) { if (process.env.TESTING) {
defaultLogger.error('Sanitized error:', detailedMessage); defaultLogger.error('Sanitized error:', detailedMessage);
@@ -37,7 +37,7 @@ function createSanitizedHttpError(statusCode, detailedMessage) {
const error = new Error(); const error = new Error();
error.status = statusCode; error.status = statusCode;
error.message = 'Permission denied'; error.message = config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage;
return error; return error;
} }

View File

@@ -31,12 +31,13 @@ const load = parseGraphQLSchema => {
const { name, schemaFields } = deepcopy(args); const { name, schemaFields } = deepcopy(args);
const { config, auth } = context; const { config, auth } = context;
enforceMasterKeyAccess(auth); enforceMasterKeyAccess(auth, config);
if (auth.isReadOnly) { if (auth.isReadOnly) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to create a schema.", "read-only masterKey isn't allowed to create a schema.",
config
); );
} }
@@ -80,12 +81,13 @@ const load = parseGraphQLSchema => {
const { name, schemaFields } = deepcopy(args); const { name, schemaFields } = deepcopy(args);
const { config, auth } = context; const { config, auth } = context;
enforceMasterKeyAccess(auth); enforceMasterKeyAccess(auth, config);
if (auth.isReadOnly) { if (auth.isReadOnly) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to update a schema." "read-only masterKey isn't allowed to update a schema.",
config
); );
} }
@@ -131,12 +133,13 @@ const load = parseGraphQLSchema => {
const { name } = deepcopy(args); const { name } = deepcopy(args);
const { config, auth } = context; const { config, auth } = context;
enforceMasterKeyAccess(auth); enforceMasterKeyAccess(auth, config);
if (auth.isReadOnly) { if (auth.isReadOnly) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to delete a schema.", "read-only masterKey isn't allowed to delete a schema.",
config
); );
} }

View File

@@ -31,7 +31,7 @@ const load = parseGraphQLSchema => {
const { name } = deepcopy(args); const { name } = deepcopy(args);
const { config, auth } = context; const { config, auth } = context;
enforceMasterKeyAccess(auth); enforceMasterKeyAccess(auth, config);
const schema = await config.database.loadSchema({ clearCache: true }); const schema = await config.database.loadSchema({ clearCache: true });
const parseClass = await getClass(name, schema); const parseClass = await getClass(name, schema);
@@ -57,7 +57,7 @@ const load = parseGraphQLSchema => {
try { try {
const { config, auth } = context; const { config, auth } = context;
enforceMasterKeyAccess(auth); enforceMasterKeyAccess(auth, config);
const schema = await config.database.loadSchema({ clearCache: true }); const schema = await config.database.loadSchema({ clearCache: true });
return (await schema.getAllClasses(true)).map(parseClass => ({ return (await schema.getAllClasses(true)).map(parseClass => ({

View File

@@ -9,7 +9,7 @@ import { createSanitizedError } from '../../Error';
const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) => { const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) => {
const { info, config } = context; const { info, config } = context;
if (!info || !info.sessionToken) { if (!info || !info.sessionToken) {
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
} }
const sessionToken = info.sessionToken; const sessionToken = info.sessionToken;
const selectedFields = getFieldNames(queryInfo) const selectedFields = getFieldNames(queryInfo)
@@ -63,7 +63,7 @@ const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) =
info.context info.context
); );
if (!response.results || response.results.length == 0) { if (!response.results || response.results.length == 0) {
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
} else { } else {
const user = response.results[0]; const user = response.results[0];
return { return {

View File

@@ -2,11 +2,12 @@ import Parse from 'parse/node';
import { GraphQLError } from 'graphql'; import { GraphQLError } from 'graphql';
import { createSanitizedError } from '../Error'; import { createSanitizedError } from '../Error';
export function enforceMasterKeyAccess(auth) { export function enforceMasterKeyAccess(auth, config) {
if (!auth.isMaster) { if (!auth.isMaster) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
'unauthorized: master key is required', 'unauthorized: master key is required',
config
); );
} }
} }

View File

@@ -247,6 +247,13 @@ module.exports.ParseServerOptions = {
action: parsers.booleanParser, action: parsers.booleanParser,
default: true, default: true,
}, },
enableSanitizedErrorResponse: {
env: 'PARSE_SERVER_ENABLE_SANITIZED_ERROR_RESPONSE',
help:
'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`.',
action: parsers.booleanParser,
default: true,
},
encodeParseObjectInCloudFunction: { encodeParseObjectInCloudFunction: {
env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION', env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION',
help: help:

View File

@@ -45,6 +45,7 @@
* @property {Boolean} enableCollationCaseComparison Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`. * @property {Boolean} enableCollationCaseComparison Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`.
* @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors * @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors
* @property {Boolean} enableInsecureAuthAdapters Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them. * @property {Boolean} enableInsecureAuthAdapters Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.
* @property {Boolean} enableSanitizedErrorResponse 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`.
* @property {Boolean} encodeParseObjectInCloudFunction If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`. <br><br> The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`. * @property {Boolean} encodeParseObjectInCloudFunction If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`. <br><br> The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.
* @property {String} encryptionKey Key for encrypting your files * @property {String} encryptionKey Key for encrypting your files
* @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access. * @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access.

View File

@@ -347,6 +347,9 @@ export interface ParseServerOptions {
rateLimit: ?(RateLimitOptions[]); rateLimit: ?(RateLimitOptions[]);
/* Options to customize the request context using inversion of control/dependency injection.*/ /* Options to customize the request context using inversion of control/dependency injection.*/
requestContextMiddleware: ?(req: any, res: any, next: any) => void; requestContextMiddleware: ?(req: any, res: any, next: any) => void;
/* 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`.
:DEFAULT: true */
enableSanitizedErrorResponse: ?boolean;
} }
export interface RateLimitOptions { export interface RateLimitOptions {

View File

@@ -52,7 +52,7 @@ async function RestQuery({
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad query type'); throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad query type');
} }
const isGet = method === RestQuery.Method.get; const isGet = method === RestQuery.Method.get;
enforceRoleSecurity(method, className, auth); enforceRoleSecurity(method, className, auth, config);
const result = runBeforeFind const result = runBeforeFind
? await triggers.maybeRunQueryTrigger( ? await triggers.maybeRunQueryTrigger(
triggers.Types.beforeFind, triggers.Types.beforeFind,
@@ -121,7 +121,7 @@ function _UnsafeRestQuery(
if (!this.auth.isMaster) { if (!this.auth.isMaster) {
if (this.className == '_Session') { if (this.className == '_Session') {
if (!this.auth.user) { if (!this.auth.user) {
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
} }
this.restWhere = { this.restWhere = {
$and: [ $and: [
@@ -424,7 +424,8 @@ _UnsafeRestQuery.prototype.validateClientClassCreation = function () {
if (hasClass !== true) { if (hasClass !== true) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, 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,
this.config
); );
} }
}); });
@@ -803,7 +804,8 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () {
if (this.restWhere[key]) { if (this.restWhere[key]) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
`This user is not allowed to query ${key} on class ${this.className}` `This user is not allowed to query ${key} on class ${this.className}`,
this.config
); );
} }
} }

View File

@@ -33,6 +33,7 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
'Cannot perform a write operation when using readOnlyMasterKey', 'Cannot perform a write operation when using readOnlyMasterKey',
config
); );
} }
this.config = config; this.config = config;
@@ -203,6 +204,7 @@ RestWrite.prototype.validateClientClassCreation = function () {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, 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,
this.config
); );
} }
}); });
@@ -662,7 +664,8 @@ RestWrite.prototype.checkRestrictedFields = async function () {
if (!this.auth.isMaintenance && !this.auth.isMaster && 'emailVerified' in this.data) { if (!this.auth.isMaintenance && !this.auth.isMaster && 'emailVerified' in this.data) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
"Clients aren't allowed to manually update email verification." "Clients aren't allowed to manually update email verification.",
this.config
); );
} }
}; };
@@ -1454,7 +1457,8 @@ RestWrite.prototype.runDatabaseOperation = function () {
if (this.className === '_User' && this.query && this.auth.isUnauthenticated()) { if (this.className === '_User' && this.query && this.auth.isUnauthenticated()) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.SESSION_MISSING, Parse.Error.SESSION_MISSING,
`Cannot modify user ${this.query.objectId}.` `Cannot modify user ${this.query.objectId}.`,
this.config
); );
} }

View File

@@ -112,7 +112,7 @@ export class ClassesRouter extends PromiseRouter {
typeof req.body?.objectId === 'string' && typeof req.body?.objectId === 'string' &&
req.body.objectId.startsWith('role:') req.body.objectId.startsWith('role:')
) { ) {
throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.'); throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.', req.config);
} }
return rest.create( return rest.create(
req.config, req.config,

View File

@@ -5,7 +5,6 @@ import Config from '../Config';
import logger from '../logger'; import logger from '../logger';
const triggers = require('../triggers'); const triggers = require('../triggers');
const Utils = require('../Utils'); const Utils = require('../Utils');
import { createSanitizedError } from '../Error';
export class FilesRouter { export class FilesRouter {
expressRouter({ maxUploadSize = '20Mb' } = {}) { expressRouter({ maxUploadSize = '20Mb' } = {}) {
@@ -44,8 +43,7 @@ export class FilesRouter {
const config = Config.get(req.params.appId); const config = Config.get(req.params.appId);
if (!config) { if (!config) {
res.status(403); res.status(403);
const err = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid application ID.'); res.json({ code: Parse.Error.OPERATION_FORBIDDEN, error: 'Invalid application ID.' });
res.json({ code: err.code, error: err.message });
return; return;
} }

View File

@@ -45,6 +45,7 @@ export class GlobalConfigRouter extends PromiseRouter {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to update the config.", "read-only masterKey isn't allowed to update the config.",
req.config
); );
} }
const params = req.body.params || {}; const params = req.body.params || {};

View File

@@ -18,6 +18,7 @@ export class GraphQLRouter extends PromiseRouter {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, 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.",
req.config
); );
} }
const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {}); const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {});

View File

@@ -9,6 +9,7 @@ export class PurgeRouter extends PromiseRouter {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to purge a schema.", "read-only masterKey isn't allowed to purge a schema.",
req.config
); );
} }
return req.config.database return req.config.database

View File

@@ -13,6 +13,7 @@ export class PushRouter extends PromiseRouter {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to send push notifications.", "read-only masterKey isn't allowed to send push notifications.",
req.config
); );
} }
const pushController = req.config.pushController; const pushController = req.config.pushController;

View File

@@ -76,6 +76,7 @@ async function createSchema(req) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to create a schema.", "read-only masterKey isn't allowed to create a schema.",
req.config
); );
} }
if (req.params.className && req.body?.className) { if (req.params.className && req.body?.className) {
@@ -98,6 +99,7 @@ function modifySchema(req) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to update a schema.", "read-only masterKey isn't allowed to update a schema.",
req.config
); );
} }
if (req.body?.className && req.body.className != req.params.className) { if (req.body?.className && req.body.className != req.params.className) {
@@ -113,6 +115,7 @@ const deleteSchema = req => {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to delete a schema.", "read-only masterKey isn't allowed to delete a schema.",
req.config
); );
} }
if (!SchemaController.classNameIsValid(req.params.className)) { if (!SchemaController.classNameIsValid(req.params.className)) {

View File

@@ -172,7 +172,7 @@ export class UsersRouter extends ClassesRouter {
handleMe(req) { handleMe(req) {
if (!req.info || !req.info.sessionToken) { if (!req.info || !req.info.sessionToken) {
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
} }
const sessionToken = req.info.sessionToken; const sessionToken = req.info.sessionToken;
return rest return rest
@@ -187,7 +187,7 @@ export class UsersRouter extends ClassesRouter {
) )
.then(response => { .then(response => {
if (!response.results || response.results.length == 0 || !response.results[0].user) { if (!response.results || response.results.length == 0 || !response.results[0].user) {
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config);
} else { } else {
const user = response.results[0].user; const user = response.results[0].user;
// Send token back on the login, because SDKs expect that. // Send token back on the login, because SDKs expect that.
@@ -338,6 +338,7 @@ export class UsersRouter extends ClassesRouter {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
'master key is required', 'master key is required',
req.config
); );
} }

View File

@@ -9,12 +9,13 @@ const classesWithMasterOnlyAccess = [
const { createSanitizedError } = require('./Error'); const { createSanitizedError } = require('./Error');
// Disallowing access to the _Role collection except by master key // Disallowing access to the _Role collection except by master key
function enforceRoleSecurity(method, className, auth) { function enforceRoleSecurity(method, className, auth, config) {
if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) { if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) {
if (method === 'delete' || method === 'find') { if (method === 'delete' || method === 'find') {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
`Clients aren't allowed to perform the ${method} operation on the installation collection.` `Clients aren't allowed to perform the ${method} operation on the installation collection.`,
config
); );
} }
} }
@@ -27,7 +28,8 @@ function enforceRoleSecurity(method, className, auth) {
) { ) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
`Clients aren't allowed to perform the ${method} operation on the ${className} collection.` `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`,
config
); );
} }
@@ -35,7 +37,8 @@ function enforceRoleSecurity(method, className, auth) {
if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) { if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) {
throw createSanitizedError( throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN, Parse.Error.OPERATION_FORBIDDEN,
`read-only masterKey isn't allowed to perform the ${method} operation.` `read-only masterKey isn't allowed to perform the ${method} operation.`,
config
); );
} }
} }

View File

@@ -502,7 +502,7 @@ export function handleParseErrors(err, req, res, next) {
export function enforceMasterKeyAccess(req, res, next) { export function enforceMasterKeyAccess(req, res, next) {
if (!req.auth.isMaster) { if (!req.auth.isMaster) {
const error = createSanitizedHttpError(403, 'unauthorized: master key is required'); const error = createSanitizedHttpError(403, 'unauthorized: master key is required', req.config);
res.status(error.status); res.status(error.status);
res.end(`{"error":"${error.message}"}`); res.end(`{"error":"${error.message}"}`);
return; return;
@@ -512,7 +512,7 @@ export function enforceMasterKeyAccess(req, res, next) {
export function promiseEnforceMasterKeyAccess(request) { export function promiseEnforceMasterKeyAccess(request) {
if (!request.auth.isMaster) { if (!request.auth.isMaster) {
throw createSanitizedHttpError(403, 'unauthorized: master key is required'); throw createSanitizedHttpError(403, 'unauthorized: master key is required', request.config);
} }
return Promise.resolve(); return Promise.resolve();
} }

View File

@@ -135,7 +135,7 @@ async function runFindTriggers(
// Returns a promise for an object with optional keys 'results' and 'count'. // Returns a promise for an object with optional keys 'results' and 'count'.
const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => { const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => {
enforceRoleSecurity('find', className, auth); enforceRoleSecurity('find', className, auth, config);
return runFindTriggers( return runFindTriggers(
config, config,
auth, auth,
@@ -150,7 +150,7 @@ const find = async (config, auth, className, restWhere, restOptions, clientSDK,
// get is just like find but only queries an objectId. // get is just like find but only queries an objectId.
const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => { const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => {
enforceRoleSecurity('get', className, auth); enforceRoleSecurity('get', className, auth, config);
return runFindTriggers( return runFindTriggers(
config, config,
auth, auth,
@@ -173,7 +173,7 @@ function del(config, auth, className, objectId, context) {
throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth to delete user'); 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 inflatedObject;
let schemaController; let schemaController;
@@ -196,7 +196,7 @@ function del(config, auth, className, objectId, context) {
firstResult.className = className; firstResult.className = className;
if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) { if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) {
if (!auth.user || firstResult.user.objectId !== auth.user.id) { if (!auth.user || firstResult.user.objectId !== auth.user.id) {
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config);
} }
} }
var cacheAdapter = config.cacheController; var cacheAdapter = config.cacheController;
@@ -258,13 +258,13 @@ function del(config, auth, className, objectId, context) {
); );
}) })
.catch(error => { .catch(error => {
handleSessionMissingError(error, className, auth); handleSessionMissingError(error, className, auth, config);
}); });
} }
// Returns a promise for a {response, status, location} object. // Returns a promise for a {response, status, location} object.
function create(config, auth, className, restObject, clientSDK, context) { 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); var write = new RestWrite(config, auth, className, null, restObject, null, clientSDK, context);
return write.execute(); return write.execute();
} }
@@ -273,7 +273,7 @@ function create(config, auth, className, restObject, clientSDK, context) {
// REST API is supposed to return. // REST API is supposed to return.
// Usually, this is just updatedAt. // Usually, this is just updatedAt.
function update(config, auth, className, restWhere, restObject, clientSDK, context) { function update(config, auth, className, restWhere, restObject, clientSDK, context) {
enforceRoleSecurity('update', className, auth); enforceRoleSecurity('update', className, auth, config);
return Promise.resolve() return Promise.resolve()
.then(async () => { .then(async () => {
@@ -315,11 +315,11 @@ function update(config, auth, className, restWhere, restObject, clientSDK, conte
).execute(); ).execute();
}) })
.catch(error => { .catch(error => {
handleSessionMissingError(error, className, auth); handleSessionMissingError(error, className, auth, config);
}); });
} }
function handleSessionMissingError(error, className, auth) { function handleSessionMissingError(error, className, auth, config) {
// If we're trying to update a user without / with bad session token // If we're trying to update a user without / with bad session token
if ( if (
className === '_User' && className === '_User' &&
@@ -327,7 +327,7 @@ function handleSessionMissingError(error, className, auth) {
!auth.isMaster && !auth.isMaster &&
!auth.isMaintenance !auth.isMaintenance
) { ) {
throw createSanitizedError(Parse.Error.SESSION_MISSING, 'Insufficient auth.'); throw createSanitizedError(Parse.Error.SESSION_MISSING, 'Insufficient auth.', config);
} }
throw error; throw error;
} }