Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions spec/ParseFile.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
31 changes: 30 additions & 1 deletion spec/Utils.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const Utils = require('../src/Utils');
const Utils = require('../lib/Utils');
const { createSanitizedError, createSanitizedHttpError } = require("../lib/Error")

describe('Utils', () => {
describe('encodeForUrl', () => {
Expand Down Expand Up @@ -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');
});
});
});
13 changes: 9 additions & 4 deletions src/Controllers/SchemaController.js
Original file line number Diff line number Diff line change
Expand Up @@ -1399,19 +1399,22 @@ 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']) {
// If aclGroup has * (public)
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
Expand All @@ -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
);
}

Expand All @@ -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
);
}

Expand Down
8 changes: 4 additions & 4 deletions src/Error.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ 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);
} else {
defaultLogger.error(detailedMessage);
}

return new Parse.Error(errorCode, 'Permission denied');
return new Parse.Error(errorCode, config.disableSanitizeError ? detailedMessage : 'Permission denied');
}

/**
Expand All @@ -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);
Expand All @@ -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;
}

Expand Down
11 changes: 7 additions & 4 deletions src/GraphQL/loaders/schemaMutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down Expand Up @@ -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
);
}

Expand Down Expand Up @@ -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
);
}

Expand Down
4 changes: 2 additions & 2 deletions src/GraphQL/loaders/schemaQueries.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 => ({
Expand Down
4 changes: 2 additions & 2 deletions src/GraphQL/loaders/usersQueries.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion src/GraphQL/parseGraphQLUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 6 additions & 4 deletions src/RestQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: [
Expand Down Expand Up @@ -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
);
}
});
Expand Down Expand Up @@ -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
);
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
);
}
});
Expand Down Expand Up @@ -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
);
}
};
Expand Down Expand Up @@ -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
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Routers/ClassesRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 1 addition & 3 deletions src/Routers/FilesRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' } = {}) {
Expand Down Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions src/Routers/GlobalConfigRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {};
Expand Down
1 change: 1 addition & 0 deletions src/Routers/GraphQLRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {});
Expand Down
1 change: 1 addition & 0 deletions src/Routers/PurgeRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/Routers/PushRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading