[7.17] Config loader: remove unecessary properties (#154902) (#155028)

# Backport

This will backport the following commits from `main` to `7.17`:
- [Config loader: remove unecessary properties
(#154902)](https://github.com/elastic/kibana/pull/154902)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Pierre
Gayvallet","email":"pierre.gayvallet@elastic.co"},"sourceCommit":{"committedDate":"2023-04-13T16:16:51Z","message":"Config
loader: remove unecessary properties (#154902)\n\n- Updates the logic of
`ensureDeepObject` to remove unnecessary\r\nproperties when expanding
the object\r\n- We had three versions of this helper, centralized it
within `@kbn/std`\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"b75d89a2d86c976d60144f8c57b1d415b2f35ac3","branchLabelMapping":{"^v8.8.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Core","release_note:skip","backport:prev-minor","v8.8.0","v8.7.1"],"number":154902,"url":"https://github.com/elastic/kibana/pull/154902","mergeCommit":{"message":"Config
loader: remove unecessary properties (#154902)\n\n- Updates the logic of
`ensureDeepObject` to remove unnecessary\r\nproperties when expanding
the object\r\n- We had three versions of this helper, centralized it
within `@kbn/std`\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"b75d89a2d86c976d60144f8c57b1d415b2f35ac3"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.8.0","labelRegex":"^v8.8.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/154902","number":154902,"mergeCommit":{"message":"Config
loader: remove unecessary properties (#154902)\n\n- Updates the logic of
`ensureDeepObject` to remove unnecessary\r\nproperties when expanding
the object\r\n- We had three versions of this helper, centralized it
within `@kbn/std`\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"b75d89a2d86c976d60144f8c57b1d415b2f35ac3"}},{"branch":"8.7","label":"v8.7.1","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/155006","number":155006,"state":"MERGED","mergeCommit":{"sha":"d9fbe142bf2a6ca7a3ef7260e2683e26a56ae846","message":"[8.7]
Config loader: remove unecessary properties (#154902) (#155006)\n\n#
Backport\n\nThis will backport the following commits from `main` to
`8.7`:\n- [Config loader: remove unecessary
properties\n(#154902)](https://github.com/elastic/kibana/pull/154902)\n\n<!---
Backport version: 8.9.7 -->\n\n### Questions ?\nPlease refer to the
[Backport
tool\ndocumentation](https://github.com/sqren/backport)\n\n<!--BACKPORT
[{\"author\":{\"name\":\"Pierre\nGayvallet\",\"email\":\"pierre.gayvallet@elastic.co\"},\"sourceCommit\":{\"committedDate\":\"2023-04-13T16:16:51Z\",\"message\":\"Config\nloader:
remove unecessary properties (#154902)\\n\\n- Updates the logic
of\n`ensureDeepObject` to remove unnecessary\\r\\nproperties when
expanding\nthe object\\r\\n- We had three versions of this helper,
centralized it\nwithin
`@kbn/std`\\r\\n\\r\\n---------\\r\\n\\r\\nCo-authored-by:
kibanamachine\n<42973632+kibanamachine@users.noreply.github.com>\",\"sha\":\"b75d89a2d86c976d60144f8c57b1d415b2f35ac3\",\"branchLabelMapping\":{\"^v8.8.0$\":\"main\",\"^v(\\\\d+).(\\\\d+).\\\\d+$\":\"$1.$2\"}},\"sourcePullRequest\":{\"labels\":[\"Team:Core\",\"release_note:skip\",\"backport:prev-minor\",\"v8.8.0\"],\"number\":154902,\"url\":\"https://github.com/elastic/kibana/pull/154902\",\"mergeCommit\":{\"message\":\"Config\nloader:
remove unecessary properties (#154902)\\n\\n- Updates the logic
of\n`ensureDeepObject` to remove unnecessary\\r\\nproperties when
expanding\nthe object\\r\\n- We had three versions of this helper,
centralized it\nwithin
`@kbn/std`\\r\\n\\r\\n---------\\r\\n\\r\\nCo-authored-by:
kibanamachine\n<42973632+kibanamachine@users.noreply.github.com>\",\"sha\":\"b75d89a2d86c976d60144f8c57b1d415b2f35ac3\"}},\"sourceBranch\":\"main\",\"suggestedTargetBranches\":[],\"targetPullRequestStates\":[{\"branch\":\"main\",\"label\":\"v8.8.0\",\"labelRegex\":\"^v8.8.0$\",\"isSourceBranch\":true,\"state\":\"MERGED\",\"url\":\"https://github.com/elastic/kibana/pull/154902\",\"number\":154902,\"mergeCommit\":{\"message\":\"Config\nloader:
remove unecessary properties (#154902)\\n\\n- Updates the logic
of\n`ensureDeepObject` to remove unnecessary\\r\\nproperties when
expanding\nthe object\\r\\n- We had three versions of this helper,
centralized it\nwithin
`@kbn/std`\\r\\n\\r\\n---------\\r\\n\\r\\nCo-authored-by:
kibanamachine\n<42973632+kibanamachine@users.noreply.github.com>\",\"sha\":\"b75d89a2d86c976d60144f8c57b1d415b2f35ac3\"}}]}]\nBACKPORT-->\n\nCo-authored-by:
Pierre Gayvallet <pierre.gayvallet@elastic.co>"}}]}] BACKPORT-->

---------

Co-authored-by: Pierre Gayvallet <pierre.gayvallet@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jean-Louis Leysens 2023-04-17 20:28:49 +02:00 committed by GitHub
parent 20e05372fa
commit 104111b178
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 129 additions and 414 deletions

View file

@ -28,6 +28,7 @@ NPM_MODULE_EXTRA_FILES = [
RUNTIME_DEPS = [
"//packages/elastic-safer-lodash-set",
"//packages/kbn-std",
"//packages/kbn-utils",
"@npm//js-yaml",
"@npm//lodash",
@ -35,6 +36,7 @@ RUNTIME_DEPS = [
TYPES_DEPS = [
"//packages/elastic-safer-lodash-set",
"//packages/kbn-std",
"//packages/kbn-utils",
"@npm//@elastic/apm-rum",
"@npm//@types/jest",

View file

@ -1,50 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
const separator = '.';
/**
* Recursively traverses through the object's properties and expands ones with
* dot-separated names into nested objects (eg. { a.b: 'c'} -> { a: { b: 'c' }).
* @param obj Object to traverse through.
* @returns Same object instance with expanded properties.
*/
export function ensureDeepObject(obj: any): any {
if (obj == null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => ensureDeepObject(item));
}
return Object.keys(obj).reduce((fullObject, propertyKey) => {
const propertyValue = obj[propertyKey];
if (!propertyKey.includes(separator)) {
fullObject[propertyKey] = ensureDeepObject(propertyValue);
} else {
walk(fullObject, propertyKey.split(separator), propertyValue);
}
return fullObject;
}, {} as any);
}
function walk(obj: any, keys: string[], value: any) {
const key = keys.shift()!;
if (keys.length === 0) {
obj[key] = value;
return;
}
if (obj[key] === undefined) {
obj[key] = {};
}
walk(obj[key], keys, ensureDeepObject(value));
}

View file

@ -10,8 +10,8 @@ import { readFileSync } from 'fs';
import { safeLoad } from 'js-yaml';
import { set } from '@elastic/safer-lodash-set';
import { ensureDeepObject } from '@kbn/std';
import { isPlainObject } from 'lodash';
import { ensureDeepObject } from './ensure_deep_object';
const readYaml = (path: string) => {
try {

View file

@ -0,0 +1,5 @@
test: {
"aaa['__proto__.hello']": "Hello",
"aaa['__proto__.nested.there']": "There",
"aaa['__proto__.nested.here']": "This JS syntax is apparently valid for our parser"
}

View file

@ -0,0 +1,3 @@
test:
hello:
'__proto__.dolly': "Well hello there"

View file

@ -1,145 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ensureDeepObject } from './ensure_deep_object';
test('flat object', () => {
const obj = {
'foo.a': 1,
'foo.b': 2,
};
expect(ensureDeepObject(obj)).toEqual({
foo: {
a: 1,
b: 2,
},
});
});
test('deep object', () => {
const obj = {
foo: {
a: 1,
b: 2,
},
};
expect(ensureDeepObject(obj)).toEqual({
foo: {
a: 1,
b: 2,
},
});
});
test('flat within deep object', () => {
const obj = {
foo: {
b: 2,
'bar.a': 1,
},
};
expect(ensureDeepObject(obj)).toEqual({
foo: {
b: 2,
bar: {
a: 1,
},
},
});
});
test('flat then flat object', () => {
const obj = {
'foo.bar': {
b: 2,
'quux.a': 1,
},
};
expect(ensureDeepObject(obj)).toEqual({
foo: {
bar: {
b: 2,
quux: {
a: 1,
},
},
},
});
});
test('full with empty array', () => {
const obj = {
a: 1,
b: [],
};
expect(ensureDeepObject(obj)).toEqual({
a: 1,
b: [],
});
});
test('full with array of primitive values', () => {
const obj = {
a: 1,
b: [1, 2, 3],
};
expect(ensureDeepObject(obj)).toEqual({
a: 1,
b: [1, 2, 3],
});
});
test('full with array of full objects', () => {
const obj = {
a: 1,
b: [{ c: 2 }, { d: 3 }],
};
expect(ensureDeepObject(obj)).toEqual({
a: 1,
b: [{ c: 2 }, { d: 3 }],
});
});
test('full with array of flat objects', () => {
const obj = {
a: 1,
b: [{ 'c.d': 2 }, { 'e.f': 3 }],
};
expect(ensureDeepObject(obj)).toEqual({
a: 1,
b: [{ c: { d: 2 } }, { e: { f: 3 } }],
});
});
test('flat with flat and array of flat objects', () => {
const obj = {
a: 1,
'b.c': 2,
d: [3, { 'e.f': 4 }, { 'g.h': 5 }],
};
expect(ensureDeepObject(obj)).toEqual({
a: 1,
b: { c: 2 },
d: [3, { e: { f: 4 } }, { g: { h: 5 } }],
});
});
test('array composed of flat objects', () => {
const arr = [{ 'c.d': 2 }, { 'e.f': 3 }];
expect(ensureDeepObject(arr)).toEqual([{ c: { d: 2 } }, { e: { f: 3 } }]);
});

View file

@ -1,50 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
const separator = '.';
/**
* Recursively traverses through the object's properties and expands ones with
* dot-separated names into nested objects (eg. { a.b: 'c'} -> { a: { b: 'c' }).
* @param obj Object to traverse through.
* @returns Same object instance with expanded properties.
*/
export function ensureDeepObject(obj: any): any {
if (obj == null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => ensureDeepObject(item));
}
return Object.keys(obj).reduce((fullObject, propertyKey) => {
const propertyValue = obj[propertyKey];
if (!propertyKey.includes(separator)) {
fullObject[propertyKey] = ensureDeepObject(propertyValue);
} else {
walk(fullObject, propertyKey.split(separator), propertyValue);
}
return fullObject;
}, {} as any);
}
function walk(obj: any, keys: string[], value: any) {
const key = keys.shift()!;
if (keys.length === 0) {
obj[key] = value;
return;
}
if (obj[key] === undefined) {
obj[key] = {};
}
walk(obj[key], keys, ensureDeepObject(value));
}

View file

@ -47,6 +47,18 @@ test('should throw an exception when referenced environment variable in a config
).toThrowErrorMatchingSnapshot();
});
test('throws parsing a config with forbidden paths', () => {
expect(() =>
getConfigFromFiles([fixtureFile('forbidden_1.yml')])
).toThrowErrorMatchingInlineSnapshot(`"Forbidden path detected: test.aaa.__proto__.hello"`);
});
test('throws parsing another config with forbidden paths', () => {
expect(() =>
getConfigFromFiles([fixtureFile('forbidden_2.yml')])
).toThrowErrorMatchingInlineSnapshot(`"Forbidden path detected: test.hello.__proto__"`);
});
describe('different cwd()', () => {
const originalCwd = process.cwd();
const tempCwd = resolve(__dirname);

View file

@ -11,7 +11,7 @@ import { safeLoad } from 'js-yaml';
import { set } from '@elastic/safer-lodash-set';
import { isPlainObject } from 'lodash';
import { ensureDeepObject } from './ensure_deep_object';
import { ensureDeepObject } from '@kbn/std';
const readYaml = (path: string) => safeLoad(readFileSync(path, 'utf8'));

View file

@ -143,3 +143,81 @@ test('array composed of flat objects', () => {
expect(ensureDeepObject(arr)).toEqual([{ c: { d: 2 } }, { e: { f: 3 } }]);
});
describe('forbidden patterns', () => {
describe('first pattern', () => {
test('throws when finding the first pattern within an object', () => {
const obj = {
foo: {
hello: 'dolly',
'bar.__proto__': { yours: 'mine' },
},
};
expect(() => ensureDeepObject(obj)).toThrowErrorMatchingInlineSnapshot(
`"Forbidden path detected: foo.bar.__proto__"`
);
});
test('throws when finding the first pattern within an array', () => {
const obj = {
array: [
'hello',
{
'bar.__proto__': { their: 'mine' },
},
],
};
expect(() => ensureDeepObject(obj)).toThrowErrorMatchingInlineSnapshot(
`"Forbidden path detected: array.1.bar.__proto__"`
);
});
});
describe('second pattern', () => {
test('throws when finding the first pattern within an object', () => {
const obj = {
foo: {
hello: 'dolly',
'bar.constructor.prototype': { foo: 'bar' },
},
};
expect(() => ensureDeepObject(obj)).toThrowErrorMatchingInlineSnapshot(
`"Forbidden path detected: foo.bar.constructor.prototype"`
);
});
test('throws when finding the first pattern within a nested object', () => {
const obj = {
foo: {
hello: 'dolly',
'bar.constructor': {
main: 'mine',
prototype: 'nope',
},
},
};
expect(() => ensureDeepObject(obj)).toThrowErrorMatchingInlineSnapshot(
`"Forbidden path detected: foo.bar.constructor.prototype"`
);
});
test('throws when finding the first pattern within an array', () => {
const obj = {
array: [
'hello',
{
'bar.constructor.prototype': { foo: 'bar' },
},
],
};
expect(() => ensureDeepObject(obj)).toThrowErrorMatchingInlineSnapshot(
`"Forbidden path detected: array.1.bar.constructor.prototype"`
);
});
});
});

View file

@ -6,56 +6,61 @@
* Side Public License, v 1.
*/
//
// THIS IS A DIRECT COPY OF
// 'packages/kbn-config/src/raw/ensure_deep_object.ts'
// BECAUSE THAT IS BLOCKED FOR IMPORTING BY OUR LINTER.
//
// IF THAT IS EXPOSED, WE SHOULD USE IT RATHER THAN CLONE IT.
//
/* eslint-disable @typescript-eslint/no-explicit-any */
// ^ Disabling the rule for the entire file because of the complexity to type this
const FORBIDDEN_PATTERNS = ['__proto__', 'constructor.prototype'];
const separator = '.';
/**
* Recursively traverses through the object's properties and expands ones with
* dot-separated names into nested objects (eg. { a.b: 'c'} -> { a: { b: 'c' }).
* @param obj Object to traverse through.
* @param path The current path of the traversal
* @returns Same object instance with expanded properties.
*/
export function ensureDeepObject(obj: any): any {
export function ensureDeepObject(obj: any, path: string[] = []): any {
assertValidPath(path);
if (obj == null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => ensureDeepObject(item));
return obj.map((item, index) => ensureDeepObject(item, [...path, `${index}`]));
}
return Object.keys(obj).reduce((fullObject, propertyKey) => {
const propertyValue = obj[propertyKey];
if (!propertyKey.includes(separator)) {
fullObject[propertyKey] = ensureDeepObject(propertyValue);
const propertySplits = propertyKey.split(separator);
if (propertySplits.length === 1) {
fullObject[propertyKey] = ensureDeepObject(propertyValue, [...path, propertyKey]);
} else {
walk(fullObject, propertyKey.split(separator), propertyValue);
walk(fullObject, propertySplits, propertyValue, path);
}
return fullObject;
}, {} as any);
}
function walk(obj: any, keys: string[], value: any) {
function walk(obj: any, keys: string[], value: any, path: string[]) {
assertValidPath([...path, ...keys]);
const key = keys.shift()!;
if (keys.length === 0) {
obj[key] = value;
return;
}
if (obj[key] === undefined) {
if (!obj.hasOwnProperty(key)) {
obj[key] = {};
}
walk(obj[key], keys, ensureDeepObject(value));
walk(obj[key], keys, ensureDeepObject(value, [...path, key, ...keys]), [...path, key]);
}
const assertValidPath = (path: string[]) => {
const flat = path.join('.');
FORBIDDEN_PATTERNS.forEach((pattern) => {
if (flat.includes(pattern)) {
throw new Error(`Forbidden path detected: ${flat}`);
}
});
};

View file

@ -19,6 +19,7 @@ export { isRelativeUrl, modifyUrl, getUrlOrigin } from './url';
export { unset } from './unset';
export { getFlattenedObject } from './get_flattened_object';
export { ensureNoUnsafeProperties } from './ensure_no_unsafe_properties';
export { ensureDeepObject } from './ensure_deep_object';
export * from './rxjs_7';
export {
map$,

View file

@ -1,145 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ensureDeepObject } from './ensure_deep_object';
test('flat object', () => {
const obj = {
'foo.a': 1,
'foo.b': 2,
};
expect(ensureDeepObject(obj)).toEqual({
foo: {
a: 1,
b: 2,
},
});
});
test('deep object', () => {
const obj = {
foo: {
a: 1,
b: 2,
},
};
expect(ensureDeepObject(obj)).toEqual({
foo: {
a: 1,
b: 2,
},
});
});
test('flat within deep object', () => {
const obj = {
foo: {
b: 2,
'bar.a': 1,
},
};
expect(ensureDeepObject(obj)).toEqual({
foo: {
b: 2,
bar: {
a: 1,
},
},
});
});
test('flat then flat object', () => {
const obj = {
'foo.bar': {
b: 2,
'quux.a': 1,
},
};
expect(ensureDeepObject(obj)).toEqual({
foo: {
bar: {
b: 2,
quux: {
a: 1,
},
},
},
});
});
test('full with empty array', () => {
const obj = {
a: 1,
b: [],
};
expect(ensureDeepObject(obj)).toEqual({
a: 1,
b: [],
});
});
test('full with array of primitive values', () => {
const obj = {
a: 1,
b: [1, 2, 3],
};
expect(ensureDeepObject(obj)).toEqual({
a: 1,
b: [1, 2, 3],
});
});
test('full with array of full objects', () => {
const obj = {
a: 1,
b: [{ c: 2 }, { d: 3 }],
};
expect(ensureDeepObject(obj)).toEqual({
a: 1,
b: [{ c: 2 }, { d: 3 }],
});
});
test('full with array of flat objects', () => {
const obj = {
a: 1,
b: [{ 'c.d': 2 }, { 'e.f': 3 }],
};
expect(ensureDeepObject(obj)).toEqual({
a: 1,
b: [{ c: { d: 2 } }, { e: { f: 3 } }],
});
});
test('flat with flat and array of flat objects', () => {
const obj = {
a: 1,
'b.c': 2,
d: [3, { 'e.f': 4 }, { 'g.h': 5 }],
};
expect(ensureDeepObject(obj)).toEqual({
a: 1,
b: { c: 2 },
d: [3, { e: { f: 4 } }, { g: { h: 5 } }],
});
});
test('array composed of flat objects', () => {
const arr = [{ 'c.d': 2 }, { 'e.f': 3 }];
expect(ensureDeepObject(arr)).toEqual([{ c: { d: 2 } }, { e: { f: 3 } }]);
});

View file

@ -10,14 +10,13 @@ import { accessSync, constants, readFileSync, statSync } from 'fs';
import { safeLoad } from 'js-yaml';
import { dirname, join } from 'path';
import { Observable } from 'rxjs';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { ensureDeepObject } from '@kbn/std';
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { take } from 'rxjs/operators';
import { TelemetryConfigType } from '../../config';
// look for telemetry.yml in the same places we expect kibana.yml
import { ensureDeepObject } from './ensure_deep_object';
import { staticTelemetrySchema } from './schema';
/**