fix: sensitive keyword detection may produce false positives (#7881)
This commit is contained in:
@@ -280,4 +280,18 @@ describe('Vulnerabilities', () => {
|
|||||||
expect(text.error).toBe('Prohibited keyword in request data: {"value":"aValue[123]*"}.');
|
expect(text.error).toBe('Prohibited keyword in request data: {"value":"aValue[123]*"}.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Ignore non-matches', () => {
|
||||||
|
it('ignores write request that contains only fraction of denied keyword', async () => {
|
||||||
|
await reconfigureServer({
|
||||||
|
requestKeywordDenylist: [{ key: 'abc' }],
|
||||||
|
});
|
||||||
|
// Initially saving an object executes the keyword detection in RestWrite.js
|
||||||
|
const obj = new TestObject({ a: { b: { c: 0 } } });
|
||||||
|
await expectAsync(obj.save()).toBeResolved();
|
||||||
|
// Modifying a nested key executes the keyword detection in DatabaseController.js
|
||||||
|
obj.increment('a.b.c');
|
||||||
|
await expectAsync(obj.save()).toBeResolved();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import intersect from 'intersect';
|
|||||||
// @flow-disable-next
|
// @flow-disable-next
|
||||||
import deepcopy from 'deepcopy';
|
import deepcopy from 'deepcopy';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
import Utils from '../Utils';
|
||||||
import * as SchemaController from './SchemaController';
|
import * as SchemaController from './SchemaController';
|
||||||
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
|
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
|
||||||
import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
|
import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||||
@@ -1763,8 +1764,8 @@ class DatabaseController {
|
|||||||
if (this.options && this.options.requestKeywordDenylist) {
|
if (this.options && this.options.requestKeywordDenylist) {
|
||||||
// Scan request data for denied keywords
|
// Scan request data for denied keywords
|
||||||
for (const keyword of this.options.requestKeywordDenylist) {
|
for (const keyword of this.options.requestKeywordDenylist) {
|
||||||
const isMatch = (a, b) => (typeof a === 'string' && new RegExp(a).test(b)) || a === b;
|
const match = Utils.objectContainsKeyValue({ firstKey: undefined }, keyword.key, undefined);
|
||||||
if (isMatch(firstKey, keyword.key)) {
|
if (match) {
|
||||||
throw new Parse.Error(
|
throw new Parse.Error(
|
||||||
Parse.Error.INVALID_KEY_NAME,
|
Parse.Error.INVALID_KEY_NAME,
|
||||||
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
|
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
|
||||||
|
|||||||
@@ -341,9 +341,9 @@ class Utils {
|
|||||||
* @returns {Boolean} True if a match was found, false otherwise.
|
* @returns {Boolean} True if a match was found, false otherwise.
|
||||||
*/
|
*/
|
||||||
static objectContainsKeyValue(obj, key, value) {
|
static objectContainsKeyValue(obj, key, value) {
|
||||||
const isMatch = (a, b) => (typeof a === 'string' && new RegExp(a).test(b)) || a === b;
|
const isMatch = (a, b) => (typeof a === 'string' && new RegExp(b).test(a)) || a === b;
|
||||||
const isKeyMatch = k => isMatch(key, k);
|
const isKeyMatch = k => isMatch(k, key);
|
||||||
const isValueMatch = v => isMatch(value, v);
|
const isValueMatch = v => isMatch(v, value);
|
||||||
for (const [k, v] of Object.entries(obj)) {
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
if (key !== undefined && value === undefined && isKeyMatch(k)) {
|
if (key !== undefined && value === undefined && isKeyMatch(k)) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user