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');