fix: security vulnerability that allows remote code execution (GHSA-p6h4-93qp-jhcm) (#7844)

This commit is contained in:
Manuel
2022-03-12 14:47:23 +01:00
committed by GitHub
parent 972b800ae4
commit e569f402b1
11 changed files with 450 additions and 46 deletions

View File

@@ -17,6 +17,7 @@ import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter';
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) {
@@ -258,41 +259,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}`;
}
@@ -395,17 +361,18 @@ const relationSchema = {
class DatabaseController {
adapter: StorageAdapter;
idempotencyOptions: any;
schemaCache: any;
schemaPromise: ?Promise<SchemaController.SchemaController>;
_transactionalSession: ?any;
options: ParseServerOptions;
idempotencyOptions: any;
constructor(adapter: StorageAdapter, idempotencyOptions?: Object = {}) {
constructor(adapter: StorageAdapter, options: ParseServerOptions) {
this.adapter = adapter;
this.idempotencyOptions = idempotencyOptions;
// 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.options = options || {};
this.idempotencyOptions = this.options.idempotencyOptions || {};
// Prevent mutable this.schema, otherwise one request could use
// multiple schemas, so instead use loadSchema to get a schema.
this.schemaPromise = null;
this._transactionalSession = null;
}
@@ -646,7 +613,7 @@ class DatabaseController {
if (skipSanitization) {
return Promise.resolve(result);
}
return sanitizeDatabaseResult(originalUpdate, result);
return this._sanitizeDatabaseResult(originalUpdate, result);
});
});
}
@@ -873,7 +840,7 @@ class DatabaseController {
object,
relationUpdates
).then(() => {
return sanitizeDatabaseResult(originalObject, result.ops[0]);
return this._sanitizeDatabaseResult(originalObject, result.ops[0]);
});
});
});
@@ -1782,6 +1749,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;
}