Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
69 changes: 69 additions & 0 deletions spec/vulnerabilities.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,75 @@ describe('Vulnerabilities', () => {
});
});

describe('Malformed $regex information disclosure', () => {
it('should not leak database error internals for invalid regex pattern in class query', async () => {
const logger = require('../lib/logger').default;
const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
const obj = new Parse.Object('TestObject');
await obj.save({ field: 'value' });

try {
await request({
method: 'GET',
url: `http://localhost:8378/1/classes/TestObject`,
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
qs: {
where: JSON.stringify({ field: { $regex: '[abc' } }),
},
});
fail('Request should have failed');
} catch (e) {
expect(e.data.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
expect(e.data.error).toBe('An internal server error occurred');
expect(typeof e.data.error).toBe('string');
expect(JSON.stringify(e.data)).not.toContain('errmsg');
expect(JSON.stringify(e.data)).not.toContain('codeName');
expect(JSON.stringify(e.data)).not.toContain('errorResponse');
expect(loggerErrorSpy).toHaveBeenCalledWith(
'Sanitized error:',
jasmine.stringMatching(/[Rr]egular expression/i)
);
}
});

it('should not leak database error internals for invalid regex pattern in role query', async () => {
const logger = require('../lib/logger').default;
const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
const role = new Parse.Role('testrole', new Parse.ACL());
await role.save(null, { useMasterKey: true });
try {
await request({
method: 'GET',
url: `http://localhost:8378/1/roles`,
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
qs: {
where: JSON.stringify({ name: { $regex: '[abc' } }),
},
});
fail('Request should have failed');
} catch (e) {
expect(e.data.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
expect(e.data.error).toBe('An internal server error occurred');
expect(typeof e.data.error).toBe('string');
expect(JSON.stringify(e.data)).not.toContain('errmsg');
expect(JSON.stringify(e.data)).not.toContain('codeName');
expect(JSON.stringify(e.data)).not.toContain('errorResponse');
expect(loggerErrorSpy).toHaveBeenCalledWith(
'Sanitized error:',
jasmine.stringMatching(/[Rr]egular expression/i)
);
}
});
});

describe('Postgres regex sanitizater', () => {
it('sanitizes the regex correctly to prevent Injection', async () => {
const user = new Parse.User();
Expand Down
15 changes: 14 additions & 1 deletion src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import SchemaCache from '../Adapters/Cache/SchemaCache';
import type { LoadSchemaOptions } from './types';
import type { ParseServerOptions } from '../Options';
import type { QueryOptions, FullQueryOptions } from '../Adapters/Storage/StorageAdapter';
import { createSanitizedError } from '../Error';

function addWriteACL(query, acl) {
const newQuery = _.cloneDeep(query);
Expand Down Expand Up @@ -1366,7 +1367,19 @@ class DatabaseController {
})
)
.catch(error => {
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, error);
if (error instanceof Parse.Error) {
throw error;
}
const detailedMessage =
typeof error === 'string'
? error
: error?.message || 'An internal server error occurred';
throw createSanitizedError(
Parse.Error.INTERNAL_SERVER_ERROR,
detailedMessage,
this.options,
'An internal server error occurred'
);
});
}
});
Expand Down
6 changes: 4 additions & 2 deletions src/Error.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ import defaultLogger from './logger';
*
* @param {number} errorCode - The Parse.Error code (e.g., Parse.Error.OPERATION_FORBIDDEN)
* @param {string} detailedMessage - The detailed error message to log server-side
* @param {object} config - Parse Server config with enableSanitizedErrorResponse
* @param {string} [sanitizedMessage='Permission denied'] - The sanitized message to return to clients
* @returns {Parse.Error} A Parse.Error with sanitized message
*/
function createSanitizedError(errorCode, detailedMessage, config) {
function createSanitizedError(errorCode, detailedMessage, config, sanitizedMessage = 'Permission denied') {
// 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, config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage);
return new Parse.Error(errorCode, config?.enableSanitizedErrorResponse !== false ? sanitizedMessage : detailedMessage);
}

/**
Expand Down
Loading