Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

forbid x-elastic-product-origin header in elasticsearch configuration #92359

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/core/server/elasticsearch/default_headers.test.ts
Original file line number Diff line number Diff line change
@@ -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(),
]);
});
});
19 changes: 16 additions & 3 deletions src/core/server/elasticsearch/default_headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
29 changes: 29 additions & 0 deletions src/core/server/elasticsearch/elasticsearch_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
39 changes: 36 additions & 3 deletions src/core/server/elasticsearch/elasticsearch_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'] });

Expand Down Expand Up @@ -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') }),
Expand Down