From 321b8bf0528c489a2836b55184dedc56d84ce369 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 25 Feb 2021 08:20:01 +0100 Subject: [PATCH] forbid x-elastic-product-origin header in elasticsearch configuration (#92359) --- .../elasticsearch/default_headers.test.ts | 23 +++++++++++ .../server/elasticsearch/default_headers.ts | 19 +++++++-- .../elasticsearch_config.test.ts | 29 ++++++++++++++ .../elasticsearch/elasticsearch_config.ts | 39 +++++++++++++++++-- 4 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 src/core/server/elasticsearch/default_headers.test.ts diff --git a/src/core/server/elasticsearch/default_headers.test.ts b/src/core/server/elasticsearch/default_headers.test.ts new file mode 100644 index 000000000000..58e6e222a3f2 --- /dev/null +++ b/src/core/server/elasticsearch/default_headers.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { getReservedHeaders, PRODUCT_ORIGIN_HEADER } from './default_headers'; + +describe('getReservedHeaders', () => { + it('returns the list of reserved headers contained in a list', () => { + expect(getReservedHeaders(['foo', 'bar', PRODUCT_ORIGIN_HEADER])).toEqual([ + PRODUCT_ORIGIN_HEADER, + ]); + }); + + it('ignores the case when identifying headers', () => { + expect(getReservedHeaders(['foo', 'bar', PRODUCT_ORIGIN_HEADER.toUpperCase()])).toEqual([ + PRODUCT_ORIGIN_HEADER.toUpperCase(), + ]); + }); +}); diff --git a/src/core/server/elasticsearch/default_headers.ts b/src/core/server/elasticsearch/default_headers.ts index 737d4772c5c0..eef04754cd95 100644 --- a/src/core/server/elasticsearch/default_headers.ts +++ b/src/core/server/elasticsearch/default_headers.ts @@ -8,9 +8,22 @@ import { deepFreeze } from '@kbn/std'; +export const PRODUCT_ORIGIN_HEADER = 'x-elastic-product-origin'; + +export const RESERVED_HEADERS = deepFreeze([PRODUCT_ORIGIN_HEADER]); + export const DEFAULT_HEADERS = deepFreeze({ // Elasticsearch uses this to identify when a request is coming from Kibana, to allow Kibana to - // access system indices using the standard ES APIs without logging a warning. After migrating to - // use the new system index APIs, this header can be removed. - 'x-elastic-product-origin': 'kibana', + // access system indices using the standard ES APIs. + [PRODUCT_ORIGIN_HEADER]: 'kibana', }); + +export const getReservedHeaders = (headerNames: string[]): string[] => { + const reservedHeaders = []; + for (const headerName of headerNames) { + if (RESERVED_HEADERS.includes(headerName.toLowerCase())) { + reservedHeaders.push(headerName); + } + } + return reservedHeaders; +}; diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index ae71932bdd3a..d3f9693bab22 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -108,6 +108,35 @@ test('#requestHeadersWhitelist accepts both string and array of strings', () => expect(configValue.requestHeadersWhitelist).toEqual(['token', 'X-Forwarded-Proto']); }); +describe('reserved headers', () => { + test('throws if customHeaders contains reserved headers', () => { + expect(() => { + config.schema.validate({ + customHeaders: { foo: 'bar', 'x-elastic-product-origin': 'beats' }, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"[customHeaders]: cannot use reserved headers: [x-elastic-product-origin]"` + ); + }); + + test('throws if requestHeadersWhitelist contains reserved headers', () => { + expect(() => { + config.schema.validate({ requestHeadersWhitelist: ['foo', 'x-elastic-product-origin'] }); + }).toThrowErrorMatchingInlineSnapshot(` + "[requestHeadersWhitelist]: types that failed validation: + - [requestHeadersWhitelist.0]: expected value of type [string] but got [Array] + - [requestHeadersWhitelist.1]: cannot use reserved headers: [x-elastic-product-origin]" + `); + expect(() => { + config.schema.validate({ requestHeadersWhitelist: 'x-elastic-product-origin' }); + }).toThrowErrorMatchingInlineSnapshot(` + "[requestHeadersWhitelist]: types that failed validation: + - [requestHeadersWhitelist.0]: cannot use reserved headers: [x-elastic-product-origin] + - [requestHeadersWhitelist.1]: could not parse array value from json input" + `); + }); +}); + describe('reads files', () => { beforeEach(() => { mockReadFileSync.mockReset(); diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 300ff4a61a28..879002a6ece5 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -12,6 +12,7 @@ import { readFileSync } from 'fs'; import { ConfigDeprecationProvider } from 'src/core/server'; import { readPkcs12Keystore, readPkcs12Truststore } from '../utils'; import { ServiceConfigDescriptor } from '../internal_types'; +import { getReservedHeaders } from './default_headers'; const hostURISchema = schema.uri({ scheme: ['http', 'https'] }); @@ -52,10 +53,42 @@ export const configSchema = schema.object({ ) ), password: schema.maybe(schema.string()), - requestHeadersWhitelist: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], { - defaultValue: ['authorization'], + requestHeadersWhitelist: schema.oneOf( + [ + schema.string({ + // can't use `validate` option on union types, forced to validate each individual subtypes + // see https://github.com/elastic/kibana/issues/64906 + validate: (headersWhitelist) => { + const reservedHeaders = getReservedHeaders([headersWhitelist]); + if (reservedHeaders.length) { + return `cannot use reserved headers: [${reservedHeaders.join(', ')}]`; + } + }, + }), + schema.arrayOf(schema.string(), { + // can't use `validate` option on union types, forced to validate each individual subtypes + // see https://github.com/elastic/kibana/issues/64906 + validate: (headersWhitelist) => { + const reservedHeaders = getReservedHeaders(headersWhitelist); + if (reservedHeaders.length) { + return `cannot use reserved headers: [${reservedHeaders.join(', ')}]`; + } + }, + }), + ], + { + defaultValue: ['authorization'], + } + ), + customHeaders: schema.recordOf(schema.string(), schema.string(), { + defaultValue: {}, + validate: (customHeaders) => { + const reservedHeaders = getReservedHeaders(Object.keys(customHeaders)); + if (reservedHeaders.length) { + return `cannot use reserved headers: [${reservedHeaders.join(', ')}]`; + } + }, }), - customHeaders: schema.recordOf(schema.string(), schema.string(), { defaultValue: {} }), shardTimeout: schema.duration({ defaultValue: '30s' }), requestTimeout: schema.duration({ defaultValue: '30s' }), pingTimeout: schema.duration({ defaultValue: schema.siblingRef('requestTimeout') }),