fix: Server internal error details leaking in error messages returned to clients (#9937)

This commit is contained in:
Lucas Coratger
2025-11-23 13:51:42 +01:00
committed by GitHub
parent 38c9d2e359
commit 50edb5ab4b
35 changed files with 390 additions and 125 deletions

View File

@@ -20,6 +20,7 @@ import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
import SchemaCache from '../Adapters/Cache/SchemaCache';
import DatabaseController from './DatabaseController';
import Config from '../Config';
import { createSanitizedError } from '../Error';
// @flow-disable-next
import deepcopy from 'deepcopy';
import type {
@@ -1403,12 +1404,12 @@ export default class SchemaController {
if (perms['requiresAuthentication']) {
// If aclGroup has * (public)
if (!aclGroup || aclGroup.length == 0) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OBJECT_NOT_FOUND,
'Permission denied, user needs to be authenticated.'
);
} else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OBJECT_NOT_FOUND,
'Permission denied, user needs to be authenticated.'
);
@@ -1425,7 +1426,7 @@ export default class SchemaController {
// Reject create when write lockdown
if (permissionField == 'writeUserFields' && operation == 'create') {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
`Permission denied for action ${operation} on class ${className}.`
);
@@ -1448,7 +1449,7 @@ export default class SchemaController {
}
}
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
`Permission denied for action ${operation} on class ${className}.`
);

44
src/Error.js Normal file
View File

@@ -0,0 +1,44 @@
import defaultLogger from './logger';
/**
* Creates a sanitized error that hides detailed information from clients
* while logging the detailed message server-side.
*
* @param {number} errorCode - The Parse.Error code (e.g., Parse.Error.OPERATION_FORBIDDEN)
* @param {string} detailedMessage - The detailed error message to log server-side
* @returns {Parse.Error} A Parse.Error with sanitized message
*/
function createSanitizedError(errorCode, detailedMessage) {
// On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file
if (process.env.TESTING) {
defaultLogger.error('Sanitized error:', detailedMessage);
} else {
defaultLogger.error(detailedMessage);
}
return new Parse.Error(errorCode, 'Permission denied');
}
/**
* Creates a sanitized error from a regular Error object
* Used for non-Parse.Error errors (e.g., Express errors)
*
* @param {number} statusCode - HTTP status code (e.g., 403)
* @param {string} detailedMessage - The detailed error message to log server-side
* @returns {Error} An Error with sanitized message
*/
function createSanitizedHttpError(statusCode, detailedMessage) {
// On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file
if (process.env.TESTING) {
defaultLogger.error('Sanitized error:', detailedMessage);
} else {
defaultLogger.error(detailedMessage);
}
const error = new Error();
error.status = statusCode;
error.message = 'Permission denied';
return error;
}
export { createSanitizedError, createSanitizedHttpError };

View File

@@ -6,6 +6,7 @@ import * as schemaTypes from './schemaTypes';
import { transformToParse, transformToGraphQL } from '../transformers/schemaFields';
import { enforceMasterKeyAccess } from '../parseGraphQLUtils';
import { getClass } from './schemaQueries';
import { createSanitizedError } from '../../Error';
const load = parseGraphQLSchema => {
const createClassMutation = mutationWithClientMutationId({
@@ -33,9 +34,9 @@ const load = parseGraphQLSchema => {
enforceMasterKeyAccess(auth);
if (auth.isReadOnly) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to create a schema."
"read-only masterKey isn't allowed to create a schema.",
);
}
@@ -82,7 +83,7 @@ const load = parseGraphQLSchema => {
enforceMasterKeyAccess(auth);
if (auth.isReadOnly) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to update a schema."
);
@@ -133,9 +134,9 @@ const load = parseGraphQLSchema => {
enforceMasterKeyAccess(auth);
if (auth.isReadOnly) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to delete a schema."
"read-only masterKey isn't allowed to delete a schema.",
);
}

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ const triggers = require('./triggers');
const { continueWhile } = require('parse/lib/node/promiseUtils');
const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
const { enforceRoleSecurity } = require('./SharedRest');
const { createSanitizedError } = require('./Error');
// restOptions can include:
// skip
@@ -120,7 +121,7 @@ function _UnsafeRestQuery(
if (!this.auth.isMaster) {
if (this.className == '_Session') {
if (!this.auth.user) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
}
this.restWhere = {
$and: [
@@ -421,7 +422,7 @@ _UnsafeRestQuery.prototype.validateClientClassCreation = function () {
.then(schemaController => schemaController.hasClass(this.className))
.then(hasClass => {
if (hasClass !== true) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
'This user is not allowed to access ' + 'non-existent class: ' + this.className
);
@@ -800,7 +801,7 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () {
) || [];
for (const key of protectedFields) {
if (this.restWhere[key]) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
`This user is not allowed to query ${key} on class ${this.className}`
);

View File

@@ -17,6 +17,7 @@ import RestQuery from './RestQuery';
import _ from 'lodash';
import logger from './logger';
import { requiredColumns } from './Controllers/SchemaController';
import { createSanitizedError } from './Error';
// query and data are both provided in REST API format. So data
// types are encoded by plain old objects.
@@ -29,9 +30,9 @@ import { requiredColumns } from './Controllers/SchemaController';
// for the _User class.
function RestWrite(config, auth, className, query, data, originalData, clientSDK, context, action) {
if (auth.isReadOnly) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
'Cannot perform a write operation when using readOnlyMasterKey'
'Cannot perform a write operation when using readOnlyMasterKey',
);
}
this.config = config;
@@ -199,9 +200,9 @@ RestWrite.prototype.validateClientClassCreation = function () {
.then(schemaController => schemaController.hasClass(this.className))
.then(hasClass => {
if (hasClass !== true) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
'This user is not allowed to access ' + 'non-existent class: ' + this.className
'This user is not allowed to access non-existent class: ' + this.className,
);
}
});
@@ -566,7 +567,6 @@ RestWrite.prototype.handleAuthData = async function (authData) {
// User found with provided authData
if (results.length === 1) {
this.storage.authProvider = Object.keys(authData).join(',');
const { hasMutatedAuthData, mutatedAuthData } = Auth.hasMutatedAuthData(
@@ -660,8 +660,10 @@ RestWrite.prototype.checkRestrictedFields = async function () {
}
if (!this.auth.isMaintenance && !this.auth.isMaster && 'emailVerified' in this.data) {
const error = `Clients aren't allowed to manually update email verification.`;
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
"Clients aren't allowed to manually update email verification."
);
}
};
@@ -1450,7 +1452,7 @@ RestWrite.prototype.runDatabaseOperation = function () {
}
if (this.className === '_User' && this.query && this.auth.isUnauthenticated()) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.SESSION_MISSING,
`Cannot modify user ${this.query.objectId}.`
);

View File

@@ -3,6 +3,7 @@ import rest from '../rest';
import _ from 'lodash';
import Parse from 'parse/node';
import { promiseEnsureIdempotency } from '../middlewares';
import { createSanitizedError } from '../Error';
const ALLOWED_GET_QUERY_KEYS = [
'keys',
@@ -111,7 +112,7 @@ export class ClassesRouter extends PromiseRouter {
typeof req.body?.objectId === 'string' &&
req.body.objectId.startsWith('role:')
) {
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.');
throw createSanitizedError(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.');
}
return rest.create(
req.config,

View File

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

View File

@@ -3,6 +3,7 @@ import Parse from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import * as middleware from '../middlewares';
import * as triggers from '../triggers';
import { createSanitizedError } from '../Error';
const getConfigFromParams = params => {
const config = new Parse.Config();
@@ -41,9 +42,9 @@ export class GlobalConfigRouter extends PromiseRouter {
async updateGlobalConfig(req) {
if (req.auth.isReadOnly) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to update the config."
"read-only masterKey isn't allowed to update the config.",
);
}
const params = req.body.params || {};

View File

@@ -1,6 +1,7 @@
import Parse from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import * as middleware from '../middlewares';
import { createSanitizedError } from '../Error';
const GraphQLConfigPath = '/graphql-config';
@@ -14,9 +15,9 @@ export class GraphQLRouter extends PromiseRouter {
async updateGraphQLConfig(req) {
if (req.auth.isReadOnly) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to update the GraphQL config."
"read-only masterKey isn't allowed to update the GraphQL config.",
);
}
const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {});

View File

@@ -1,13 +1,14 @@
import PromiseRouter from '../PromiseRouter';
import * as middleware from '../middlewares';
import Parse from 'parse/node';
import { createSanitizedError } from '../Error';
export class PurgeRouter extends PromiseRouter {
handlePurge(req) {
if (req.auth.isReadOnly) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to purge a schema."
"read-only masterKey isn't allowed to purge a schema.",
);
}
return req.config.database

View File

@@ -1,6 +1,7 @@
import PromiseRouter from '../PromiseRouter';
import * as middleware from '../middlewares';
import { Parse } from 'parse/node';
import { createSanitizedError } from '../Error';
export class PushRouter extends PromiseRouter {
mountRoutes() {
@@ -9,9 +10,9 @@ export class PushRouter extends PromiseRouter {
static handlePOST(req) {
if (req.auth.isReadOnly) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to send push notifications."
"read-only masterKey isn't allowed to send push notifications.",
);
}
const pushController = req.config.pushController;

View File

@@ -5,6 +5,7 @@ var Parse = require('parse/node').Parse,
import PromiseRouter from '../PromiseRouter';
import * as middleware from '../middlewares';
import { createSanitizedError } from '../Error';
function classNameMismatchResponse(bodyClass, pathClass) {
throw new Parse.Error(
@@ -72,9 +73,9 @@ export const internalUpdateSchema = async (className, body, config) => {
async function createSchema(req) {
checkIfDefinedSchemasIsUsed(req);
if (req.auth.isReadOnly) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to create a schema."
"read-only masterKey isn't allowed to create a schema.",
);
}
if (req.params.className && req.body?.className) {
@@ -94,9 +95,9 @@ async function createSchema(req) {
function modifySchema(req) {
checkIfDefinedSchemasIsUsed(req);
if (req.auth.isReadOnly) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to update a schema."
"read-only masterKey isn't allowed to update a schema.",
);
}
if (req.body?.className && req.body.className != req.params.className) {
@@ -109,9 +110,9 @@ function modifySchema(req) {
const deleteSchema = req => {
if (req.auth.isReadOnly) {
throw new Parse.Error(
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to delete a schema."
"read-only masterKey isn't allowed to delete a schema.",
);
}
if (!SchemaController.classNameIsValid(req.params.className)) {

View File

@@ -17,6 +17,7 @@ import {
import { promiseEnsureIdempotency } from '../middlewares';
import RestWrite from '../RestWrite';
import { logger } from '../logger';
import { createSanitizedError } from '../Error';
export class UsersRouter extends ClassesRouter {
className() {
@@ -171,7 +172,7 @@ export class UsersRouter extends ClassesRouter {
handleMe(req) {
if (!req.info || !req.info.sessionToken) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
}
const sessionToken = req.info.sessionToken;
return rest
@@ -186,7 +187,7 @@ export class UsersRouter extends ClassesRouter {
)
.then(response => {
if (!response.results || response.results.length == 0 || !response.results[0].user) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
} else {
const user = response.results[0].user;
// Send token back on the login, because SDKs expect that.
@@ -334,7 +335,10 @@ export class UsersRouter extends ClassesRouter {
*/
async handleLogInAs(req) {
if (!req.auth.isMaster) {
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'master key is required');
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
'master key is required',
);
}
const userId = req.body?.userId || req.query.userId;

View File

@@ -6,12 +6,16 @@ const classesWithMasterOnlyAccess = [
'_JobSchedule',
'_Idempotency',
];
const { createSanitizedError } = require('./Error');
// Disallowing access to the _Role collection except by master key
function enforceRoleSecurity(method, className, auth) {
if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) {
if (method === 'delete' || method === 'find') {
const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`;
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
`Clients aren't allowed to perform the ${method} operation on the installation collection.`
);
}
}
@@ -21,14 +25,18 @@ function enforceRoleSecurity(method, className, auth) {
!auth.isMaster &&
!auth.isMaintenance
) {
const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`;
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
`Clients aren't allowed to perform the ${method} operation on the ${className} collection.`
);
}
// readOnly masterKey is not allowed
if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) {
const error = `read-only masterKey isn't allowed to perform the ${method} operation.`;
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
`read-only masterKey isn't allowed to perform the ${method} operation.`
);
}
}

View File

@@ -81,3 +81,4 @@ export class Connections {
return this.sockets.size;
}
}

View File

@@ -13,6 +13,7 @@ import { pathToRegexp } from 'path-to-regexp';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';
import { BlockList, isIPv4 } from 'net';
import { createSanitizedHttpError } from './Error';
export const DEFAULT_ALLOWED_HEADERS =
'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, X-Parse-Request-Id, Content-Type, Pragma, Cache-Control';
@@ -501,8 +502,9 @@ export function handleParseErrors(err, req, res, next) {
export function enforceMasterKeyAccess(req, res, next) {
if (!req.auth.isMaster) {
res.status(403);
res.end('{"error":"unauthorized: master key is required"}');
const error = createSanitizedHttpError(403, 'unauthorized: master key is required');
res.status(error.status);
res.end(`{"error":"${error.message}"}`);
return;
}
next();
@@ -510,10 +512,7 @@ export function enforceMasterKeyAccess(req, res, next) {
export function promiseEnforceMasterKeyAccess(request) {
if (!request.auth.isMaster) {
const error = new Error();
error.status = 403;
error.message = 'unauthorized: master key is required';
throw error;
throw createSanitizedHttpError(403, 'unauthorized: master key is required');
}
return Promise.resolve();
}

View File

@@ -13,6 +13,7 @@ var RestQuery = require('./RestQuery');
var RestWrite = require('./RestWrite');
var triggers = require('./triggers');
const { enforceRoleSecurity } = require('./SharedRest');
const { createSanitizedError } = require('./Error');
function checkTriggers(className, config, types) {
return types.some(triggerType => {
@@ -195,7 +196,7 @@ function del(config, auth, className, objectId, context) {
firstResult.className = className;
if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) {
if (!auth.user || firstResult.user.objectId !== auth.user.id) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
}
}
var cacheAdapter = config.cacheController;
@@ -326,7 +327,7 @@ function handleSessionMissingError(error, className, auth) {
!auth.isMaster &&
!auth.isMaintenance
) {
throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth.');
throw createSanitizedError(Parse.Error.SESSION_MISSING, 'Insufficient auth.');
}
throw error;
}