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

This commit is contained in:
Manuel
2022-03-12 13:49:57 +01:00
committed by GitHub
parent a48015c3b0
commit 971adb5438
11 changed files with 445 additions and 40 deletions

View File

@@ -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;
}