From 50592d79c251ee6bca9cca4d4742420067fb075f Mon Sep 17 00:00:00 2001 From: coratgerl <73360179+coratgerl@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:59:23 +0100 Subject: [PATCH 1/5] feat: add option to disable sanitizedError --- spec/ParseFile.spec.js | 4 +--- spec/Utils.spec.js | 31 +++++++++++++++++++++++++- src/Controllers/SchemaController.js | 13 +++++++---- src/Error.js | 8 +++---- src/GraphQL/loaders/schemaMutations.js | 11 +++++---- src/GraphQL/loaders/schemaQueries.js | 4 ++-- src/GraphQL/loaders/usersQueries.js | 4 ++-- src/GraphQL/parseGraphQLUtils.js | 3 ++- src/Options/Definitions.js | 7 ++++++ src/Options/docs.js | 1 + src/Options/index.js | 3 +++ src/RestQuery.js | 10 +++++---- src/RestWrite.js | 8 +++++-- src/Routers/ClassesRouter.js | 2 +- src/Routers/FilesRouter.js | 4 +--- src/Routers/GlobalConfigRouter.js | 1 + src/Routers/GraphQLRouter.js | 1 + src/Routers/PurgeRouter.js | 1 + src/Routers/PushRouter.js | 1 + src/Routers/SchemasRouter.js | 3 +++ src/Routers/UsersRouter.js | 5 +++-- src/SharedRest.js | 11 +++++---- src/middlewares.js | 4 ++-- src/rest.js | 20 ++++++++--------- 24 files changed, 111 insertions(+), 49 deletions(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 7e6f5e5080..5c1c3c99e7 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -767,13 +767,11 @@ 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: 'Permission denied' }); - expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('Invalid application ID.')); + expect(res1.data).toEqual({ code: 119, error: '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/Utils.spec.js b/spec/Utils.spec.js index 2bbc5656a2..aac65fd5da 100644 --- a/spec/Utils.spec.js +++ b/spec/Utils.spec.js @@ -1,4 +1,5 @@ -const Utils = require('../src/Utils'); +const Utils = require('../lib/Utils'); +const { createSanitizedError, createSanitizedHttpError } = require("../lib/Error") describe('Utils', () => { describe('encodeForUrl', () => { @@ -173,4 +174,32 @@ describe('Utils', () => { expect(Utils.getNestedProperty(obj, 'database.name')).toBe(''); }); }); + + describe('createSanitizedError', () => { + it('should return "Permission denied" when disableSanitizeError is false or undefined', () => { + const config = { disableSanitizeError: false }; + const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', config); + expect(error.message).toBe('Permission denied'); + }); + + it('should return the detailed message when disableSanitizeError is true', () => { + const config = { disableSanitizeError: true }; + 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 disableSanitizeError is false or undefined', () => { + const config = { disableSanitizeError: false }; + const error = createSanitizedHttpError(403, 'Detailed error message', config); + expect(error.message).toBe('Permission denied'); + }); + + it('should return the detailed message when disableSanitizeError is true', () => { + const config = { disableSanitizeError: true }; + const error = createSanitizedHttpError(403, 'Detailed error message', config); + expect(error.message).toBe('Detailed error message'); + }); + }); }); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 70694925bf..b605fba632 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -1399,6 +1399,7 @@ export default class SchemaController { return true; } const perms = classPermissions[operation]; + const config = Config.get(Parse.applicationId) // If only for authenticated users // make sure we have an aclGroup if (perms['requiresAuthentication']) { @@ -1406,12 +1407,14 @@ export default class SchemaController { if (!aclGroup || aclGroup.length == 0) { throw createSanitizedError( 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) { throw createSanitizedError( 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 @@ -1428,7 +1431,8 @@ export default class SchemaController { if (permissionField == 'writeUserFields' && operation == 'create') { throw createSanitizedError( 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( Parse.Error.OPERATION_FORBIDDEN, - `Permission denied for action ${operation} on class ${className}.` + `Permission denied for action ${operation} on class ${className}.`, + config ); } diff --git a/src/Error.js b/src/Error.js index 4729965e1c..3abfcdc998 100644 --- a/src/Error.js +++ b/src/Error.js @@ -8,7 +8,7 @@ import defaultLogger from './logger'; * @param {string} detailedMessage - The detailed error message to log server-side * @returns {Parse.Error} A Parse.Error with sanitized message */ -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 if (process.env.TESTING) { defaultLogger.error('Sanitized error:', detailedMessage); @@ -16,7 +16,7 @@ function createSanitizedError(errorCode, detailedMessage) { defaultLogger.error(detailedMessage); } - return new Parse.Error(errorCode, 'Permission denied'); + return new Parse.Error(errorCode, config.disableSanitizeError ? detailedMessage : 'Permission denied'); } /** @@ -27,7 +27,7 @@ function createSanitizedError(errorCode, detailedMessage) { * @param {string} detailedMessage - The detailed error message to log server-side * @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 if (process.env.TESTING) { defaultLogger.error('Sanitized error:', detailedMessage); @@ -37,7 +37,7 @@ function createSanitizedHttpError(statusCode, detailedMessage) { const error = new Error(); error.status = statusCode; - error.message = 'Permission denied'; + error.message = config.disableSanitizeError ? detailedMessage : 'Permission denied'; return error; } diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index 5dd8969bd9..93cd89d54a 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -31,12 +31,13 @@ const load = parseGraphQLSchema => { const { name, schemaFields } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); if (auth.isReadOnly) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to create a schema.", + config ); } @@ -80,12 +81,13 @@ const load = parseGraphQLSchema => { const { name, schemaFields } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); if (auth.isReadOnly) { 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.", + config ); } @@ -131,12 +133,13 @@ const load = parseGraphQLSchema => { const { name } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); if (auth.isReadOnly) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to delete a schema.", + config ); } diff --git a/src/GraphQL/loaders/schemaQueries.js b/src/GraphQL/loaders/schemaQueries.js index 25bc071919..2956b47934 100644 --- a/src/GraphQL/loaders/schemaQueries.js +++ b/src/GraphQL/loaders/schemaQueries.js @@ -31,7 +31,7 @@ const load = parseGraphQLSchema => { const { name } = deepcopy(args); const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); const schema = await config.database.loadSchema({ clearCache: true }); const parseClass = await getClass(name, schema); @@ -57,7 +57,7 @@ const load = parseGraphQLSchema => { try { const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth, config); const schema = await config.database.loadSchema({ clearCache: true }); return (await schema.getAllClasses(true)).map(parseClass => ({ diff --git a/src/GraphQL/loaders/usersQueries.js b/src/GraphQL/loaders/usersQueries.js index a51e9553c0..dc9f57f5ef 100644 --- a/src/GraphQL/loaders/usersQueries.js +++ b/src/GraphQL/loaders/usersQueries.js @@ -9,7 +9,7 @@ import { createSanitizedError } from '../../Error'; const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) => { const { info, config } = context; 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 selectedFields = getFieldNames(queryInfo) @@ -63,7 +63,7 @@ const getUserFromSessionToken = async (context, queryInfo, keysPrefix, userId) = info.context ); 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 { const user = response.results[0]; return { diff --git a/src/GraphQL/parseGraphQLUtils.js b/src/GraphQL/parseGraphQLUtils.js index 1a0b266a36..ba5fd1b416 100644 --- a/src/GraphQL/parseGraphQLUtils.js +++ b/src/GraphQL/parseGraphQLUtils.js @@ -2,11 +2,12 @@ import Parse from 'parse/node'; import { GraphQLError } from 'graphql'; import { createSanitizedError } from '../Error'; -export function enforceMasterKeyAccess(auth) { +export function enforceMasterKeyAccess(auth, config) { if (!auth.isMaster) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, 'unauthorized: master key is required', + config ); } } diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 6eeff0ed57..2e374a0c43 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -199,6 +199,13 @@ module.exports.ParseServerOptions = { action: parsers.booleanParser, default: true, }, + disableSanitizeError: { + env: 'PARSE_SERVER_DISABLE_SANITIZE_ERROR', + help: + 'If true, disables sanitizing errors and returns the detailed message instead of "Permission denied".', + action: parsers.booleanParser, + default: false, + }, dotNetKey: { env: 'PARSE_SERVER_DOT_NET_KEY', help: 'Key for Unity and .Net SDK', diff --git a/src/Options/docs.js b/src/Options/docs.js index 03fa9cc981..f4eb07535a 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -37,6 +37,7 @@ * @property {String} databaseURI The full URI to your database. Supported databases are mongodb or postgres. * @property {Number} defaultLimit Default value for limit option on queries, defaults to `100`. * @property {Boolean} directAccess Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`.

If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports.

⚠️ In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`. + * @property {Boolean} disableSanitizeError If true, disables sanitizing errors and returns the detailed message instead of "Permission denied". * @property {String} dotNetKey Key for Unity and .Net SDK * @property {Adapter} emailAdapter Adapter module for email sending * @property {Boolean} emailVerifyTokenReuseIfValid Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.
Requires option `verifyUserEmails: true`. diff --git a/src/Options/index.js b/src/Options/index.js index 11ab00a00f..587473363e 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -347,6 +347,9 @@ export interface ParseServerOptions { rateLimit: ?(RateLimitOptions[]); /* Options to customize the request context using inversion of control/dependency injection.*/ requestContextMiddleware: ?(req: any, res: any, next: any) => void; + /* If true, disables sanitizing errors and returns the detailed message instead of "Permission denied". + :DEFAULT: false */ + disableSanitizeError: ?boolean; } export interface RateLimitOptions { diff --git a/src/RestQuery.js b/src/RestQuery.js index b102caea23..2064ffd0df 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -52,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, @@ -121,7 +121,7 @@ function _UnsafeRestQuery( if (!this.auth.isMaster) { if (this.className == '_Session') { 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 = { $and: [ @@ -424,7 +424,8 @@ _UnsafeRestQuery.prototype.validateClientClassCreation = function () { if (hasClass !== true) { 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, + this.config ); } }); @@ -803,7 +804,8 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () { if (this.restWhere[key]) { throw createSanitizedError( 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 ); } } diff --git a/src/RestWrite.js b/src/RestWrite.js index c8c9584fde..a0de5577a5 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -33,6 +33,7 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, 'Cannot perform a write operation when using readOnlyMasterKey', + config ); } this.config = config; @@ -203,6 +204,7 @@ RestWrite.prototype.validateClientClassCreation = function () { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, '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) { throw createSanitizedError( 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()) { throw createSanitizedError( Parse.Error.SESSION_MISSING, - `Cannot modify user ${this.query.objectId}.` + `Cannot modify user ${this.query.objectId}.`, + this.config ); } diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 09dc85022b..83db6dbab0 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -112,7 +112,7 @@ export class ClassesRouter extends PromiseRouter { typeof req.body?.objectId === 'string' && 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( req.config, diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index e2b9271192..f0bb483d7b 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -5,7 +5,6 @@ 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' } = {}) { @@ -44,8 +43,7 @@ export class FilesRouter { const config = Config.get(req.params.appId); if (!config) { res.status(403); - const err = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid application ID.'); - res.json({ code: err.code, error: err.message }); + res.json({ code: Parse.Error.OPERATION_FORBIDDEN, error: 'Invalid application ID.' }); return; } diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index 4b107ee878..6a05f7308f 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -45,6 +45,7 @@ export class GlobalConfigRouter extends PromiseRouter { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to update the config.", + req.config ); } const params = req.body.params || {}; diff --git a/src/Routers/GraphQLRouter.js b/src/Routers/GraphQLRouter.js index d785afbdf2..67d7a24e46 100644 --- a/src/Routers/GraphQLRouter.js +++ b/src/Routers/GraphQLRouter.js @@ -18,6 +18,7 @@ export class GraphQLRouter extends PromiseRouter { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to update the GraphQL config.", + req.config ); } const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {}); diff --git a/src/Routers/PurgeRouter.js b/src/Routers/PurgeRouter.js index 7b992a48e2..f346d64176 100644 --- a/src/Routers/PurgeRouter.js +++ b/src/Routers/PurgeRouter.js @@ -9,6 +9,7 @@ export class PurgeRouter extends PromiseRouter { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to purge a schema.", + req.config ); } return req.config.database diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js index 123677f138..696f19ed88 100644 --- a/src/Routers/PushRouter.js +++ b/src/Routers/PushRouter.js @@ -13,6 +13,7 @@ export class PushRouter extends PromiseRouter { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to send push notifications.", + req.config ); } const pushController = req.config.pushController; diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index ff55711a69..8713c95518 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -76,6 +76,7 @@ async function createSchema(req) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to create a schema.", + req.config ); } if (req.params.className && req.body?.className) { @@ -98,6 +99,7 @@ function modifySchema(req) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to update a schema.", + req.config ); } if (req.body?.className && req.body.className != req.params.className) { @@ -113,6 +115,7 @@ const deleteSchema = req => { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, "read-only masterKey isn't allowed to delete a schema.", + req.config ); } if (!SchemaController.classNameIsValid(req.params.className)) { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index f50f9608d2..3828e465e7 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -172,7 +172,7 @@ export class UsersRouter extends ClassesRouter { handleMe(req) { 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; return rest @@ -187,7 +187,7 @@ export class UsersRouter extends ClassesRouter { ) .then(response => { 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 { const user = response.results[0].user; // Send token back on the login, because SDKs expect that. @@ -338,6 +338,7 @@ export class UsersRouter extends ClassesRouter { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, 'master key is required', + req.config ); } diff --git a/src/SharedRest.js b/src/SharedRest.js index 1d342b595a..3dc396d30c 100644 --- a/src/SharedRest.js +++ b/src/SharedRest.js @@ -9,12 +9,13 @@ const classesWithMasterOnlyAccess = [ const { createSanitizedError } = require('./Error'); // 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 (method === 'delete' || method === 'find') { throw createSanitizedError( 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( 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')) { throw createSanitizedError( 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 ); } } diff --git a/src/middlewares.js b/src/middlewares.js index 2da7016b4f..2fedce8f08 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -502,7 +502,7 @@ export function handleParseErrors(err, req, res, next) { export function enforceMasterKeyAccess(req, res, next) { 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.end(`{"error":"${error.message}"}`); return; @@ -512,7 +512,7 @@ export function enforceMasterKeyAccess(req, res, next) { export function promiseEnforceMasterKeyAccess(request) { 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(); } diff --git a/src/rest.js b/src/rest.js index 66feae66f0..66763715ea 100644 --- a/src/rest.js +++ b/src/rest.js @@ -135,7 +135,7 @@ async function runFindTriggers( // Returns a promise for an object with optional keys 'results' and 'count'. const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => { - enforceRoleSecurity('find', className, auth); + enforceRoleSecurity('find', className, auth, config); return runFindTriggers( config, auth, @@ -150,7 +150,7 @@ const find = async (config, auth, className, restWhere, restOptions, clientSDK, // get is just like find but only queries an objectId. const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => { - enforceRoleSecurity('get', className, auth); + enforceRoleSecurity('get', className, auth, config); return runFindTriggers( config, auth, @@ -173,7 +173,7 @@ function del(config, auth, className, objectId, context) { throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth to delete user'); } - enforceRoleSecurity('delete', className, auth); + enforceRoleSecurity('delete', className, auth, config); let inflatedObject; let schemaController; @@ -196,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 createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', config); } } var cacheAdapter = config.cacheController; @@ -258,13 +258,13 @@ function del(config, auth, className, objectId, context) { ); }) .catch(error => { - handleSessionMissingError(error, className, auth); + handleSessionMissingError(error, className, auth, config); }); } // Returns a promise for a {response, status, location} object. function create(config, auth, className, restObject, clientSDK, context) { - enforceRoleSecurity('create', className, auth); + enforceRoleSecurity('create', className, auth, config); var write = new RestWrite(config, auth, className, null, restObject, null, clientSDK, context); return write.execute(); } @@ -273,7 +273,7 @@ function create(config, auth, className, restObject, clientSDK, context) { // REST API is supposed to return. // Usually, this is just updatedAt. function update(config, auth, className, restWhere, restObject, clientSDK, context) { - enforceRoleSecurity('update', className, auth); + enforceRoleSecurity('update', className, auth, config); return Promise.resolve() .then(async () => { @@ -315,11 +315,11 @@ function update(config, auth, className, restWhere, restObject, clientSDK, conte ).execute(); }) .catch(error => { - handleSessionMissingError(error, className, auth); + handleSessionMissingError(error, className, auth, config); }); } -function handleSessionMissingError(error, className, auth) { +function handleSessionMissingError(error, className, auth, config) { // If we're trying to update a user without / with bad session token if ( className === '_User' && @@ -327,7 +327,7 @@ function handleSessionMissingError(error, className, auth) { !auth.isMaster && !auth.isMaintenance ) { - throw createSanitizedError(Parse.Error.SESSION_MISSING, 'Insufficient auth.'); + throw createSanitizedError(Parse.Error.SESSION_MISSING, 'Insufficient auth.', config); } throw error; } From a0a32eec8ecbe169d96a5ecb185f764bf168b7de Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Tue, 25 Nov 2025 07:51:30 +0100 Subject: [PATCH 2/5] fix: undefined case --- spec/Utils.spec.js | 10 ++++++++++ src/Error.js | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/spec/Utils.spec.js b/spec/Utils.spec.js index aac65fd5da..12c43c35d3 100644 --- a/spec/Utils.spec.js +++ b/spec/Utils.spec.js @@ -182,6 +182,11 @@ describe('Utils', () => { 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 disableSanitizeError is true', () => { const config = { disableSanitizeError: true }; const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', config); @@ -196,6 +201,11 @@ describe('Utils', () => { 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 disableSanitizeError is true', () => { const config = { disableSanitizeError: true }; const error = createSanitizedHttpError(403, 'Detailed error message', config); diff --git a/src/Error.js b/src/Error.js index 3abfcdc998..51322a4012 100644 --- a/src/Error.js +++ b/src/Error.js @@ -16,7 +16,7 @@ function createSanitizedError(errorCode, detailedMessage, config) { defaultLogger.error(detailedMessage); } - return new Parse.Error(errorCode, config.disableSanitizeError ? detailedMessage : 'Permission denied'); + return new Parse.Error(errorCode, config?.disableSanitizeError ? detailedMessage : 'Permission denied'); } /** @@ -37,7 +37,7 @@ function createSanitizedHttpError(statusCode, detailedMessage, config) { const error = new Error(); error.status = statusCode; - error.message = config.disableSanitizeError ? detailedMessage : 'Permission denied'; + error.message = config?.disableSanitizeError ? detailedMessage : 'Permission denied'; return error; } From 7ba6b7cf2e966f267e7f98b7f3982bedb06e5d3d Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:55:44 +0100 Subject: [PATCH 3/5] rename option Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- src/Options/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Options/index.js b/src/Options/index.js index 587473363e..cdeb7cd846 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -347,9 +347,9 @@ export interface ParseServerOptions { rateLimit: ?(RateLimitOptions[]); /* Options to customize the request context using inversion of control/dependency injection.*/ requestContextMiddleware: ?(req: any, res: any, next: any) => void; - /* If true, disables sanitizing errors and returns the detailed message instead of "Permission denied". - :DEFAULT: false */ - disableSanitizeError: ?boolean; + /* 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 { From 19c002585317c072326f5f4aceef66fe2366fd50 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:59:55 +0100 Subject: [PATCH 4/5] fix: feedbacks --- spec/Utils.spec.js | 16 ++++++++-------- src/Error.js | 4 ++-- src/Options/Definitions.js | 14 +++++++------- src/Options/docs.js | 2 +- src/Options/index.js | 6 +++--- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/spec/Utils.spec.js b/spec/Utils.spec.js index 12c43c35d3..644638d28e 100644 --- a/spec/Utils.spec.js +++ b/spec/Utils.spec.js @@ -176,8 +176,8 @@ describe('Utils', () => { }); describe('createSanitizedError', () => { - it('should return "Permission denied" when disableSanitizeError is false or undefined', () => { - const config = { disableSanitizeError: false }; + it('should return "Permission denied" when enableSanitizedErrorResponse is false or undefined', () => { + const config = { enableSanitizedErrorResponse: true }; const error = createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Detailed error message', config); expect(error.message).toBe('Permission denied'); }); @@ -187,16 +187,16 @@ describe('Utils', () => { expect(error.message).toBe('Permission denied'); }); - it('should return the detailed message when disableSanitizeError is true', () => { - const config = { disableSanitizeError: true }; + it('should return the detailed message when enableSanitizedErrorResponse is true', () => { + 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 disableSanitizeError is false or undefined', () => { - const config = { disableSanitizeError: false }; + it('should return "Permission denied" when enableSanitizedErrorResponse is false or undefined', () => { + const config = { enableSanitizedErrorResponse: true }; const error = createSanitizedHttpError(403, 'Detailed error message', config); expect(error.message).toBe('Permission denied'); }); @@ -206,8 +206,8 @@ describe('Utils', () => { expect(error.message).toBe('Permission denied'); }); - it('should return the detailed message when disableSanitizeError is true', () => { - const config = { disableSanitizeError: true }; + it('should return the detailed message when enableSanitizedErrorResponse is true', () => { + const config = { enableSanitizedErrorResponse: false }; const error = createSanitizedHttpError(403, 'Detailed error message', config); expect(error.message).toBe('Detailed error message'); }); diff --git a/src/Error.js b/src/Error.js index 51322a4012..75eff3d673 100644 --- a/src/Error.js +++ b/src/Error.js @@ -16,7 +16,7 @@ function createSanitizedError(errorCode, detailedMessage, config) { defaultLogger.error(detailedMessage); } - return new Parse.Error(errorCode, config?.disableSanitizeError ? detailedMessage : 'Permission denied'); + return new Parse.Error(errorCode, config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage); } /** @@ -37,7 +37,7 @@ function createSanitizedHttpError(statusCode, detailedMessage, config) { const error = new Error(); error.status = statusCode; - error.message = config?.disableSanitizeError ? detailedMessage : 'Permission denied'; + error.message = config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage; return error; } diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 2e374a0c43..774b4505e1 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -199,13 +199,6 @@ module.exports.ParseServerOptions = { action: parsers.booleanParser, default: true, }, - disableSanitizeError: { - env: 'PARSE_SERVER_DISABLE_SANITIZE_ERROR', - help: - 'If true, disables sanitizing errors and returns the detailed message instead of "Permission denied".', - action: parsers.booleanParser, - default: false, - }, dotNetKey: { env: 'PARSE_SERVER_DOT_NET_KEY', help: 'Key for Unity and .Net SDK', @@ -254,6 +247,13 @@ module.exports.ParseServerOptions = { action: parsers.booleanParser, 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: { env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION', help: diff --git a/src/Options/docs.js b/src/Options/docs.js index f4eb07535a..9569239ef7 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -37,7 +37,6 @@ * @property {String} databaseURI The full URI to your database. Supported databases are mongodb or postgres. * @property {Number} defaultLimit Default value for limit option on queries, defaults to `100`. * @property {Boolean} directAccess Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`.

If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports.

⚠️ In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`. - * @property {Boolean} disableSanitizeError If true, disables sanitizing errors and returns the detailed message instead of "Permission denied". * @property {String} dotNetKey Key for Unity and .Net SDK * @property {Adapter} emailAdapter Adapter module for email sending * @property {Boolean} emailVerifyTokenReuseIfValid Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.
Requires option `verifyUserEmails: true`. @@ -46,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} 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} 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`.

ℹ️ 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 {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access. diff --git a/src/Options/index.js b/src/Options/index.js index 587473363e..cdeb7cd846 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -347,9 +347,9 @@ export interface ParseServerOptions { rateLimit: ?(RateLimitOptions[]); /* Options to customize the request context using inversion of control/dependency injection.*/ requestContextMiddleware: ?(req: any, res: any, next: any) => void; - /* If true, disables sanitizing errors and returns the detailed message instead of "Permission denied". - :DEFAULT: false */ - disableSanitizeError: ?boolean; + /* 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 { From 47401e48edfa63aaac3cff105589d73fbd8acc38 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:11:25 +0100 Subject: [PATCH 5/5] fix: feedbacks --- spec/Utils.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/Utils.spec.js b/spec/Utils.spec.js index 644638d28e..a473064376 100644 --- a/spec/Utils.spec.js +++ b/spec/Utils.spec.js @@ -176,7 +176,7 @@ describe('Utils', () => { }); describe('createSanitizedError', () => { - it('should return "Permission denied" when enableSanitizedErrorResponse is false or undefined', () => { + 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'); @@ -187,7 +187,7 @@ describe('Utils', () => { expect(error.message).toBe('Permission denied'); }); - it('should return the detailed message when enableSanitizedErrorResponse is true', () => { + 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'); @@ -195,7 +195,7 @@ describe('Utils', () => { }); describe('createSanitizedHttpError', () => { - it('should return "Permission denied" when enableSanitizedErrorResponse is false or undefined', () => { + 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'); @@ -206,7 +206,7 @@ describe('Utils', () => { expect(error.message).toBe('Permission denied'); }); - it('should return the detailed message when enableSanitizedErrorResponse is true', () => { + 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');