fix: security vulnerability that allows remote code execution (GHSA-p6h4-93qp-jhcm) (#7843)
This commit is contained in:
@@ -16,6 +16,7 @@ import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
|
||||
import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||
import SchemaCache from '../Adapters/Cache/SchemaCache';
|
||||
import type { LoadSchemaOptions } from './types';
|
||||
import type { ParseServerOptions } from '../Options';
|
||||
import type { QueryOptions, FullQueryOptions } from '../Adapters/Storage/StorageAdapter';
|
||||
|
||||
function addWriteACL(query, acl) {
|
||||
@@ -257,41 +258,6 @@ const isSpecialUpdateKey = key => {
|
||||
return specialKeysForUpdate.indexOf(key) >= 0;
|
||||
};
|
||||
|
||||
function expandResultOnKeyPath(object, key, value) {
|
||||
if (key.indexOf('.') < 0) {
|
||||
object[key] = value[key];
|
||||
return object;
|
||||
}
|
||||
const path = key.split('.');
|
||||
const firstKey = path[0];
|
||||
const nextPath = path.slice(1).join('.');
|
||||
object[firstKey] = expandResultOnKeyPath(object[firstKey] || {}, nextPath, value[firstKey]);
|
||||
delete object[key];
|
||||
return object;
|
||||
}
|
||||
|
||||
function sanitizeDatabaseResult(originalObject, result): Promise<any> {
|
||||
const response = {};
|
||||
if (!result) {
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
Object.keys(originalObject).forEach(key => {
|
||||
const keyUpdate = originalObject[key];
|
||||
// determine if that was an op
|
||||
if (
|
||||
keyUpdate &&
|
||||
typeof keyUpdate === 'object' &&
|
||||
keyUpdate.__op &&
|
||||
['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1
|
||||
) {
|
||||
// only valid ops that produce an actionable result
|
||||
// the op may have happend on a keypath
|
||||
expandResultOnKeyPath(response, key, result);
|
||||
}
|
||||
});
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
function joinTableName(className, key) {
|
||||
return `_Join:${key}:${className}`;
|
||||
}
|
||||
@@ -397,14 +363,16 @@ class DatabaseController {
|
||||
schemaCache: any;
|
||||
schemaPromise: ?Promise<SchemaController.SchemaController>;
|
||||
_transactionalSession: ?any;
|
||||
options: ParseServerOptions;
|
||||
|
||||
constructor(adapter: StorageAdapter) {
|
||||
constructor(adapter: StorageAdapter, options: ParseServerOptions) {
|
||||
this.adapter = adapter;
|
||||
// We don't want a mutable this.schema, because then you could have
|
||||
// one request that uses different schemas for different parts of
|
||||
// it. Instead, use loadSchema to get a schema.
|
||||
this.schemaPromise = null;
|
||||
this._transactionalSession = null;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
collectionExists(className: string): Promise<boolean> {
|
||||
@@ -643,7 +611,7 @@ class DatabaseController {
|
||||
if (skipSanitization) {
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
return sanitizeDatabaseResult(originalUpdate, result);
|
||||
return this._sanitizeDatabaseResult(originalUpdate, result);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -870,7 +838,7 @@ class DatabaseController {
|
||||
object,
|
||||
relationUpdates
|
||||
).then(() => {
|
||||
return sanitizeDatabaseResult(originalObject, result.ops[0]);
|
||||
return this._sanitizeDatabaseResult(originalObject, result.ops[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1771,6 +1739,60 @@ class DatabaseController {
|
||||
await this.adapter.updateSchemaWithIndexes();
|
||||
}
|
||||
|
||||
_expandResultOnKeyPath(object: any, key: string, value: any): any {
|
||||
if (key.indexOf('.') < 0) {
|
||||
object[key] = value[key];
|
||||
return object;
|
||||
}
|
||||
const path = key.split('.');
|
||||
const firstKey = path[0];
|
||||
const nextPath = path.slice(1).join('.');
|
||||
|
||||
// Scan request data for denied keywords
|
||||
if (this.options && this.options.requestKeywordDenylist) {
|
||||
// Scan request data for denied keywords
|
||||
for (const keyword of this.options.requestKeywordDenylist) {
|
||||
const isMatch = (a, b) => (typeof a === 'string' && new RegExp(a).test(b)) || a === b;
|
||||
if (isMatch(firstKey, keyword.key)) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object[firstKey] = this._expandResultOnKeyPath(
|
||||
object[firstKey] || {},
|
||||
nextPath,
|
||||
value[firstKey]
|
||||
);
|
||||
delete object[key];
|
||||
return object;
|
||||
}
|
||||
|
||||
_sanitizeDatabaseResult(originalObject: any, result: any): Promise<any> {
|
||||
const response = {};
|
||||
if (!result) {
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
Object.keys(originalObject).forEach(key => {
|
||||
const keyUpdate = originalObject[key];
|
||||
// determine if that was an op
|
||||
if (
|
||||
keyUpdate &&
|
||||
typeof keyUpdate === 'object' &&
|
||||
keyUpdate.__op &&
|
||||
['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1
|
||||
) {
|
||||
// only valid ops that produce an actionable result
|
||||
// the op may have happened on a keypath
|
||||
this._expandResultOnKeyPath(response, key, result);
|
||||
}
|
||||
});
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
static _validateQuery: any => void;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user