-
Notifications
You must be signed in to change notification settings - Fork 918
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[CVE] Handle invalid query, index and date in vega charts filter hand…
…lers (#1932) * [CVE] Handle invalid query, index and date in vega charts filter handlers Potential way to prevent XSS vulnerability discovered in the Vega charts OSD integration. CVE link: https://nvd.nist.gov/vuln/detail/CVE-2022-23713 Signed-off-by: Bandini Bhopi <bandinib@amazon.com> * new license header for new files Signed-off-by: Bandini Bhopi <bandinib@amazon.com> Co-authored-by: Kawika Avilla <kavilla414@gmail.com> (cherry picked from commit 9496da3)
- Loading branch information
1 parent
683a738
commit b585e3f
Showing
7 changed files
with
290 additions
and
8 deletions.
There are no files selected for viewing
13 changes: 13 additions & 0 deletions
13
packages/osd-std/src/__snapshots__/validate_object.test.ts.snap
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { validateObject } from './validate_object'; | ||
|
||
test(`fails on circular references`, () => { | ||
const foo: Record<string, any> = {}; | ||
foo.myself = foo; | ||
|
||
expect(() => | ||
validateObject({ | ||
payload: foo, | ||
}) | ||
).toThrowErrorMatchingInlineSnapshot(`"circular reference detected"`); | ||
}); | ||
|
||
[ | ||
{ | ||
foo: true, | ||
bar: '__proto__', | ||
baz: 1.1, | ||
qux: undefined, | ||
quux: () => null, | ||
quuz: Object.create(null), | ||
}, | ||
{ | ||
foo: { | ||
foo: true, | ||
bar: '__proto__', | ||
baz: 1.1, | ||
qux: undefined, | ||
quux: () => null, | ||
quuz: Object.create(null), | ||
}, | ||
}, | ||
{ constructor: { foo: { prototype: null } } }, | ||
{ prototype: { foo: { constructor: null } } }, | ||
].forEach((value) => { | ||
['headers', 'payload', 'query', 'params'].forEach((property) => { | ||
const obj = { | ||
[property]: value, | ||
}; | ||
test(`can submit ${JSON.stringify(obj)}`, () => { | ||
expect(() => validateObject(obj)).not.toThrowError(); | ||
}); | ||
}); | ||
}); | ||
|
||
// if we use the object literal syntax to create the following values, we end up | ||
// actually reassigning the __proto__ which makes it be a non-enumerable not-own property | ||
// which isn't what we want to test here | ||
[ | ||
JSON.parse(`{ "__proto__": null }`), | ||
JSON.parse(`{ "foo": { "__proto__": true } }`), | ||
JSON.parse(`{ "foo": { "bar": { "__proto__": {} } } }`), | ||
JSON.parse(`{ "constructor": { "prototype" : null } }`), | ||
JSON.parse(`{ "foo": { "constructor": { "prototype" : null } } }`), | ||
JSON.parse(`{ "foo": { "bar": { "constructor": { "prototype" : null } } } }`), | ||
].forEach((value) => { | ||
test(`can't submit ${JSON.stringify(value)}`, () => { | ||
expect(() => validateObject(value)).toThrowErrorMatchingSnapshot(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
interface StackItem { | ||
value: any; | ||
previousKey: string | null; | ||
} | ||
|
||
// we have to do Object.prototype.hasOwnProperty because when you create an object using | ||
// Object.create(null), and I assume other methods, you get an object without a prototype, | ||
// so you can't use current.hasOwnProperty | ||
const hasOwnProperty = (obj: any, property: string) => | ||
Object.prototype.hasOwnProperty.call(obj, property); | ||
|
||
const isObject = (obj: any) => typeof obj === 'object' && obj !== null; | ||
|
||
// we're using a stack instead of recursion so we aren't limited by the call stack | ||
export function validateObject(obj: any) { | ||
if (!isObject(obj)) { | ||
return; | ||
} | ||
|
||
const stack: StackItem[] = [ | ||
{ | ||
value: obj, | ||
previousKey: null, | ||
}, | ||
]; | ||
const seen = new WeakSet([obj]); | ||
|
||
while (stack.length > 0) { | ||
const { value, previousKey } = stack.pop()!; | ||
|
||
if (!isObject(value)) { | ||
continue; | ||
} | ||
|
||
if (hasOwnProperty(value, '__proto__')) { | ||
throw new Error(`'__proto__' is an invalid key`); | ||
} | ||
|
||
if (hasOwnProperty(value, 'prototype') && previousKey === 'constructor') { | ||
throw new Error(`'constructor.prototype' is an invalid key`); | ||
} | ||
|
||
// iterating backwards through an array is reportedly more performant | ||
const entries = Object.entries(value); | ||
for (let i = entries.length - 1; i >= 0; --i) { | ||
const [key, childValue] = entries[i]; | ||
if (isObject(childValue)) { | ||
if (seen.has(childValue)) { | ||
throw new Error('circular reference detected'); | ||
} | ||
|
||
seen.add(childValue); | ||
} | ||
|
||
stack.push({ | ||
value: childValue, | ||
previousKey: key, | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { Utils } from './utils'; | ||
|
||
describe('Utils.handleNonStringIndex', () => { | ||
test('should return same string on string input', async () => { | ||
expect(Utils.handleNonStringIndex('*')).toBe('*'); | ||
}); | ||
|
||
test('should return empty string on empty string input', async () => { | ||
expect(Utils.handleNonStringIndex('')).toBe(''); | ||
}); | ||
|
||
test('should return undefined on non-string input', async () => { | ||
expect(Utils.handleNonStringIndex(123)).toBe(undefined); | ||
expect(Utils.handleNonStringIndex(null)).toBe(undefined); | ||
expect(Utils.handleNonStringIndex(undefined)).toBe(undefined); | ||
}); | ||
}); | ||
|
||
describe('Utils.handleInvalidDate', () => { | ||
test('should return null if passed timestamp is Not A Number', async () => { | ||
expect(Utils.handleInvalidDate(Number.NaN)).toBe(null); | ||
}); | ||
|
||
test('should return timestamp if passed timestamp is valid Number', async () => { | ||
expect(Utils.handleInvalidDate(1658189958487)).toBe(1658189958487); | ||
}); | ||
|
||
test('should return string on string input', async () => { | ||
expect(Utils.handleInvalidDate('Sat Jul 16 2022 22:59:42')).toBe('Sat Jul 16 2022 22:59:42'); | ||
}); | ||
|
||
test('should return date on date input', async () => { | ||
const date = Date.now(); | ||
expect(Utils.handleInvalidDate(date)).toBe(date); | ||
}); | ||
|
||
test('should return null if input neigther timestamp, nor date, nor string', async () => { | ||
expect(Utils.handleInvalidDate(undefined)).toBe(null); | ||
expect(Utils.handleInvalidDate({ key: 'value' })).toBe(null); | ||
}); | ||
}); | ||
|
||
describe('Utils.handleInvalidQuery', () => { | ||
test('should return valid object on valid DSL query', async () => { | ||
const testQuery = { match_phrase: { customer_gender: 'MALE' } }; | ||
expect(Utils.handleInvalidQuery(testQuery)).toStrictEqual(testQuery); | ||
}); | ||
|
||
test('should return null on null or undefined input', async () => { | ||
expect(Utils.handleInvalidQuery(null)).toBe(null); | ||
expect(Utils.handleInvalidQuery(undefined)).toBe(null); | ||
}); | ||
|
||
test('should return null if input object has function as property', async () => { | ||
const input = { | ||
key1: 'value1', | ||
key2: () => { | ||
alert('Hello!'); | ||
}, | ||
}; | ||
|
||
expect(Utils.handleInvalidQuery(input)).toBe(null); | ||
}); | ||
|
||
test('should return null if nested object has function as property', async () => { | ||
const input = { | ||
key1: 'value1', | ||
key2: { | ||
func: () => { | ||
alert('Hello!'); | ||
}, | ||
}, | ||
}; | ||
expect(Utils.handleInvalidQuery(input)).toBe(null); | ||
}); | ||
|
||
test('should throw error on polluted query', async () => { | ||
const maliciousQueries = [ | ||
JSON.parse(`{ "__proto__": null }`), | ||
JSON.parse(`{ "constructor": { "prototype" : null } }`), | ||
]; | ||
|
||
maliciousQueries.forEach((value) => { | ||
expect(() => { | ||
Utils.handleInvalidQuery(value); | ||
}).toThrowError(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters