-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Enables preventing access to internal APIs #156935
Changes from 26 commits
2f8af4e
34a0e5f
3adc1df
a8ca696
dc0f209
5a7463f
e200669
4d90466
0a0cb44
e823131
6070213
7d6038a
59583ba
78a69ea
8a5fedd
12a46cb
30c441f
5324c20
3cae610
78c0fc0
1a9247b
01e4f1e
f5b2a27
cb86813
e321d1b
f38d8bd
24ff4c3
0093f59
3bd27de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -160,6 +160,7 @@ describe('Fetch', () => { | |
expect(fetchMock.lastOptions()!.headers).toMatchObject({ | ||
'content-type': 'application/json', | ||
'kbn-version': 'VERSION', | ||
'x-elastic-internal-origin': 'Kibana', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're not concerned about the content here, only that the header is present. |
||
myheader: 'foo', | ||
}); | ||
}); | ||
|
@@ -168,13 +169,30 @@ describe('Fetch', () => { | |
fetchMock.get('*', {}); | ||
await expect( | ||
fetchInstance.fetch('/my/path', { | ||
headers: { myHeader: 'foo', 'kbn-version': 'CUSTOM!' }, | ||
headers: { | ||
myHeader: 'foo', | ||
'kbn-version': 'CUSTOM!', | ||
}, | ||
}) | ||
).rejects.toThrowErrorMatchingInlineSnapshot( | ||
`"Invalid fetch headers, headers beginning with \\"kbn-\\" are not allowed: [kbn-version]"` | ||
); | ||
}); | ||
|
||
it('should not allow overwriting of x-elastic-internal-origin header', async () => { | ||
fetchMock.get('*', {}); | ||
await expect( | ||
fetchInstance.fetch('/my/path', { | ||
headers: { | ||
myHeader: 'foo', | ||
'x-elastic-internal-origin': 'anything', | ||
}, | ||
}) | ||
).rejects.toThrowErrorMatchingInlineSnapshot( | ||
`"Invalid fetch headers, headers beginning with \\"x-elastic-internal-\\" are not allowed: [x-elastic-internal-origin]"` | ||
); | ||
}); | ||
|
||
it('should not set kbn-system-request header by default', async () => { | ||
fetchMock.get('*', {}); | ||
await fetchInstance.fetch('/my/path', { | ||
|
@@ -310,6 +328,7 @@ describe('Fetch', () => { | |
headers: { | ||
'content-type': 'application/json', | ||
'kbn-version': 'VERSION', | ||
'x-elastic-internal-origin': 'Kibana', | ||
}, | ||
}); | ||
}); | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -150,6 +150,7 @@ const configSchema = schema.object( | |
}, | ||
} | ||
), | ||
restrictInternalApis: schema.boolean({ defaultValue: false }), // allow access to internal routes by default to prevent breaking changes in current offerings | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code comment to explain the intent. |
||
}, | ||
{ | ||
validate: (rawConfig) => { | ||
|
@@ -223,6 +224,7 @@ export class HttpConfig implements IHttpConfig { | |
public xsrf: { disableProtection: boolean; allowlist: string[] }; | ||
public requestId: { allowFromAnyIp: boolean; ipAllowlist: string[] }; | ||
public shutdownTimeout: Duration; | ||
public restrictInternalApis: boolean; | ||
|
||
/** | ||
* @internal | ||
|
@@ -263,6 +265,7 @@ export class HttpConfig implements IHttpConfig { | |
this.xsrf = rawHttpConfig.xsrf; | ||
this.requestId = rawHttpConfig.requestId; | ||
this.shutdownTimeout = rawHttpConfig.shutdownTimeout; | ||
this.restrictInternalApis = rawHttpConfig.restrictInternalApis; | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
import { Env } from '@kbn/config'; | ||
import type { OnPostAuthHandler, OnPreResponseHandler } from '@kbn/core-http-server'; | ||
import { isSafeMethod } from '@kbn/core-http-router-server-internal'; | ||
import { X_ELASTIC_INTERNAL_ORIGIN_REQUEST } from '@kbn/core-http-common/src/constants'; | ||
import { HttpConfig } from './http_config'; | ||
import { LifecycleRegistrar } from './http_server'; | ||
|
||
|
@@ -39,6 +40,27 @@ export const createXsrfPostAuthHandler = (config: HttpConfig): OnPostAuthHandler | |
}; | ||
}; | ||
|
||
export const createRestrictInternalRoutesPostAuthHandler = ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This takes a similar approach to |
||
config: HttpConfig | ||
): OnPostAuthHandler => { | ||
const isRestrictionEnabled = config.restrictInternalApis; | ||
|
||
return (request, response, toolkit) => { | ||
const isInternalRoute = request.route.options.access === 'internal'; | ||
|
||
// only check if the header is present, not it's content. | ||
const hasInternalKibanaRequestHeader = X_ELASTIC_INTERNAL_ORIGIN_REQUEST in request.headers; | ||
|
||
if (isRestrictionEnabled && isInternalRoute && !hasInternalKibanaRequestHeader) { | ||
TinaHeiligers marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// throw 400 | ||
return response.badRequest({ | ||
body: `uri [${request.url}] with method [${request.route.method}] exists but is not available with the current configuration`, | ||
TinaHeiligers marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
} | ||
return toolkit.next(); | ||
}; | ||
}; | ||
|
||
export const createVersionCheckPostAuthHandler = (kibanaVersion: string): OnPostAuthHandler => { | ||
return (request, response, toolkit) => { | ||
const requestVersion = request.headers[VERSION_HEADER]; | ||
|
@@ -60,7 +82,6 @@ export const createVersionCheckPostAuthHandler = (kibanaVersion: string): OnPost | |
}; | ||
}; | ||
|
||
// TODO: implement header required for accessing internal routes. See https://github.com/elastic/kibana/issues/151940 | ||
export const createCustomHeadersPreResponseHandler = (config: HttpConfig): OnPreResponseHandler => { | ||
const { | ||
name: serverName, | ||
|
@@ -76,7 +97,6 @@ export const createCustomHeadersPreResponseHandler = (config: HttpConfig): OnPre | |
'Content-Security-Policy': cspHeader, | ||
[KIBANA_NAME_HEADER]: serverName, | ||
}; | ||
|
||
return toolkit.next({ headers: additionalHeaders }); | ||
}; | ||
}; | ||
|
@@ -86,7 +106,12 @@ export const registerCoreHandlers = ( | |
config: HttpConfig, | ||
env: Env | ||
) => { | ||
// add headers based on config | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't often update these and I added comments to more easily follow where they're applied. |
||
registrar.registerOnPreResponse(createCustomHeadersPreResponseHandler(config)); | ||
// add extra request checks stuff | ||
registrar.registerOnPostAuth(createXsrfPostAuthHandler(config)); | ||
// add check on version | ||
registrar.registerOnPostAuth(createVersionCheckPostAuthHandler(env.packageInfo.version)); | ||
// add check on header if the route is internal | ||
registrar.registerOnPostAuth(createRestrictInternalRoutesPostAuthHandler(config)); // strictly speaking, we should have access to route.options.access from the request on postAuth | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -157,7 +157,9 @@ export interface BulkGetItem { | |
export function isKibanaRequest({ headers }: KibanaRequest) { | ||
// The presence of these two request headers gives us a good indication that this is a first-party request from the Kibana client. | ||
// We can't be 100% certain, but this is a reasonable attempt. | ||
return headers && headers['kbn-version'] && headers.referer; | ||
return ( | ||
headers && headers['kbn-version'] && headers.referer && headers['x-elastic-internal-origin'] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ATM, there are 4: core usage stats client, cases plugin, rule registry, and spaces usage stats client. I've added the header to core's implementation. Are the concerns Joe raised still val a valid reason to have 4 implementations of A while back, Joe raised concerns about adding a header to all Kibana requests:
If these are still valid, then having different implementations is warrented. @response-ops @kibana-security would a single method fullfil your needs or are concerns around false negatives be an issue for you? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO (1) and (2) should be mitigated because we will not be restricting access to internal endpoints by default so everything should continue working as-is for on-prem. |
||
); | ||
} | ||
|
||
export interface LogWarnOnExternalRequest { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added here for intended purpose. Once all intra-stack components and Kibana browser-side code not using Core's http service are updated, we can enable the restriction.