mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
# Backport This will backport the following commits from `main` to `8.17`: - [Additional prototype pollution protections (#206073)](https://github.com/elastic/kibana/pull/206073) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Larry Gregory","email":"larry.gregory@elastic.co"},"sourceCommit":{"committedDate":"2025-01-28T22:00:43Z","message":"Additional prototype pollution protections (#206073)\n\n## Summary\n\n1. Extends the server-side prototype pollution protections introduced in\nhttps://github.com/elastic/kibana/pull/190716 to include\n`Array.prototype`.\n2. Applies the same prototype pollution protections to the client-side.\n\n\n### Identify risks\n\nDoes this PR introduce any risks? For example, consider risks like hard\nto test bugs, performance regression, potential of data loss.\n\nDescribe the risk, its severity, and mitigation for each identified\nrisk. Invite stakeholders and evaluate how to proceed before merging.\n\n- [ ] Sealing prototypes on the client can lead to failures in\nthird-party dependencies. I'm relying on sufficient functional test\ncoverage to detect issues here. As a result, these protections are\ndisabled by default for now, and can be controlled via setting\n`server.prototypeHardening: true/false`\n\n---------\n\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"9ce2dd8df9f2bd6c0ba1d089b69ddfd7fc1f4a02","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Security","release_note:skip","Feature:Hardening","v9.0.0","backport:prev-minor","ci:cloud-deploy","ci:project-deploy-elasticsearch","backport:version","v8.17.0","v8.18.0","ci:all-gen-ai-suites","v9.1.0"],"title":"Additional prototype pollution protections","number":206073,"url":"https://github.com/elastic/kibana/pull/206073","mergeCommit":{"message":"Additional prototype pollution protections (#206073)\n\n## Summary\n\n1. Extends the server-side prototype pollution protections introduced in\nhttps://github.com/elastic/kibana/pull/190716 to include\n`Array.prototype`.\n2. Applies the same prototype pollution protections to the client-side.\n\n\n### Identify risks\n\nDoes this PR introduce any risks? For example, consider risks like hard\nto test bugs, performance regression, potential of data loss.\n\nDescribe the risk, its severity, and mitigation for each identified\nrisk. Invite stakeholders and evaluate how to proceed before merging.\n\n- [ ] Sealing prototypes on the client can lead to failures in\nthird-party dependencies. I'm relying on sufficient functional test\ncoverage to detect issues here. As a result, these protections are\ndisabled by default for now, and can be controlled via setting\n`server.prototypeHardening: true/false`\n\n---------\n\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"9ce2dd8df9f2bd6c0ba1d089b69ddfd7fc1f4a02"}},"sourceBranch":"main","suggestedTargetBranches":["8.17"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/206073","number":206073,"mergeCommit":{"message":"Additional prototype pollution protections (#206073)\n\n## Summary\n\n1. Extends the server-side prototype pollution protections introduced in\nhttps://github.com/elastic/kibana/pull/190716 to include\n`Array.prototype`.\n2. Applies the same prototype pollution protections to the client-side.\n\n\n### Identify risks\n\nDoes this PR introduce any risks? For example, consider risks like hard\nto test bugs, performance regression, potential of data loss.\n\nDescribe the risk, its severity, and mitigation for each identified\nrisk. Invite stakeholders and evaluate how to proceed before merging.\n\n- [ ] Sealing prototypes on the client can lead to failures in\nthird-party dependencies. I'm relying on sufficient functional test\ncoverage to detect issues here. As a result, these protections are\ndisabled by default for now, and can be controlled via setting\n`server.prototypeHardening: true/false`\n\n---------\n\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"9ce2dd8df9f2bd6c0ba1d089b69ddfd7fc1f4a02"}},{"branch":"8.17","label":"v8.17.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/208742","number":208742,"state":"MERGED","mergeCommit":{"sha":"24f82ee808ca45130b44e062aabbaf9475ca7a58","message":"[8.x] Additional prototype pollution protections (#206073) (#208742)\n\n# Backport\n\nThis will backport the following commits from `main` to `8.x`:\n- [Additional prototype pollution protections\n(#206073)](https://github.com/elastic/kibana/pull/206073)\n\n\n\n### Questions ?\nPlease refer to the [Backport tool\ndocumentation](https://github.com/sorenlouv/backport)\n\n"}},{"branch":"9.1","label":"v9.1.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> --------- Co-authored-by: Larry Gregory <larry.gregory@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
7746e727fc
commit
bbb6a64691
39 changed files with 361 additions and 85 deletions
|
@ -1222,7 +1222,7 @@
|
|||
"react": "^17.0.2",
|
||||
"react-diff-view": "^3.2.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropzone": "^4.2.9",
|
||||
"react-dropzone": "^11.7.1",
|
||||
"react-fast-compare": "^2.0.4",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-hook-form": "^7.44.2",
|
||||
|
|
|
@ -84,6 +84,7 @@ Object {
|
|||
"payloadTimeout": 20000,
|
||||
"port": 5601,
|
||||
"protocol": "http1",
|
||||
"prototypeHardening": false,
|
||||
"requestId": Object {
|
||||
"allowFromAnyIp": false,
|
||||
"ipAllowlist": Array [],
|
||||
|
|
|
@ -644,6 +644,16 @@ describe('http2 protocol', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('prototypeHardening', () => {
|
||||
it('defaults to false', () => {
|
||||
expect(config.schema.validate({}).prototypeHardening).toBe(false);
|
||||
});
|
||||
|
||||
it('can be set to true', () => {
|
||||
expect(config.schema.validate({ prototypeHardening: true }).prototypeHardening).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HttpConfig', () => {
|
||||
it('converts customResponseHeaders to strings or arrays of strings', () => {
|
||||
const httpSchema = config.schema;
|
||||
|
|
|
@ -126,6 +126,7 @@ const configSchema = schema.object(
|
|||
protocol: schema.oneOf([schema.literal('http1'), schema.literal('http2')], {
|
||||
defaultValue: 'http1',
|
||||
}),
|
||||
prototypeHardening: schema.boolean({ defaultValue: false }),
|
||||
host: schema.string({
|
||||
defaultValue: 'localhost',
|
||||
hostname: true,
|
||||
|
@ -327,6 +328,7 @@ export class HttpConfig implements IHttpConfig {
|
|||
brotli: { enabled: boolean; quality: number };
|
||||
};
|
||||
public csp: ICspConfig;
|
||||
public prototypeHardening: boolean;
|
||||
public externalUrl: IExternalUrlConfig;
|
||||
public xsrf: { disableProtection: boolean; allowlist: string[] };
|
||||
public requestId: { allowFromAnyIp: boolean; ipAllowlist: string[] };
|
||||
|
@ -380,6 +382,7 @@ export class HttpConfig implements IHttpConfig {
|
|||
this.compression = rawHttpConfig.compression;
|
||||
this.cdn = CdnConfig.from(rawHttpConfig.cdn);
|
||||
this.csp = new CspConfig({ ...rawCspConfig, disableEmbedding }, this.cdn.getCspConfig());
|
||||
this.prototypeHardening = rawHttpConfig.prototypeHardening;
|
||||
this.externalUrl = rawExternalUrlConfig;
|
||||
this.xsrf = rawHttpConfig.xsrf;
|
||||
this.requestId = rawHttpConfig.requestId;
|
||||
|
|
|
@ -136,6 +136,7 @@ export interface HttpServerSetup {
|
|||
staticAssets: InternalStaticAssets;
|
||||
basePath: HttpServiceSetup['basePath'];
|
||||
csp: HttpServiceSetup['csp'];
|
||||
prototypeHardening: boolean;
|
||||
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
|
||||
registerOnPreRouting: HttpServiceSetup['registerOnPreRouting'];
|
||||
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
|
||||
|
@ -302,6 +303,7 @@ export class HttpServer {
|
|||
),
|
||||
basePath: basePathService,
|
||||
csp: config.csp,
|
||||
prototypeHardening: config.prototypeHardening,
|
||||
auth: {
|
||||
get: this.authState.get,
|
||||
isAuthenticated: this.authState.isAuthenticated,
|
||||
|
|
|
@ -129,6 +129,7 @@ export class HttpService
|
|||
this.internalPreboot = {
|
||||
externalUrl: new ExternalUrlConfig(config.externalUrl),
|
||||
csp: prebootSetup.csp,
|
||||
prototypeHardening: prebootSetup.prototypeHardening,
|
||||
staticAssets: prebootSetup.staticAssets,
|
||||
basePath: prebootSetup.basePath,
|
||||
registerStaticDir: prebootSetup.registerStaticDir.bind(prebootSetup),
|
||||
|
|
|
@ -37,6 +37,7 @@ export interface InternalHttpServicePreboot
|
|||
| 'registerRouteHandlerContext'
|
||||
| 'server'
|
||||
| 'getServerInfo'
|
||||
| 'prototypeHardening'
|
||||
> {
|
||||
registerRoutes<
|
||||
DefaultRequestHandlerType extends RequestHandlerContextBase = RequestHandlerContextBase
|
||||
|
@ -53,6 +54,7 @@ export interface InternalHttpServiceSetup
|
|||
server: HttpServerSetup['server'];
|
||||
staticAssets: InternalStaticAssets;
|
||||
externalUrl: ExternalUrlConfig;
|
||||
prototypeHardening: boolean;
|
||||
createRouter: <Context extends RequestHandlerContextBase = RequestHandlerContextBase>(
|
||||
path: string,
|
||||
plugin?: PluginOpaqueId
|
||||
|
|
|
@ -127,6 +127,7 @@ const createInternalPrebootContractMock = (args: CreateMockArgs = {}) => {
|
|||
basePath,
|
||||
staticAssets: createInternalStaticAssetsMock(basePath, args.cdnUrl),
|
||||
csp: CspConfig.DEFAULT,
|
||||
prototypeHardening: false,
|
||||
externalUrl: ExternalUrlConfig.DEFAULT,
|
||||
auth: createAuthMock(),
|
||||
getServerInfo: jest.fn(),
|
||||
|
@ -182,6 +183,7 @@ const createInternalSetupContractMock = () => {
|
|||
registerStaticDir: jest.fn(),
|
||||
basePath,
|
||||
csp: CspConfig.DEFAULT,
|
||||
prototypeHardening: false,
|
||||
staticAssets: createInternalStaticAssetsMock(basePath),
|
||||
externalUrl: ExternalUrlConfig.DEFAULT,
|
||||
auth: createAuthMock(),
|
||||
|
|
|
@ -32,6 +32,8 @@ function kbnBundlesLoader() {
|
|||
}
|
||||
|
||||
var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data'));
|
||||
var kbnHardenPrototypes = JSON.parse(document.querySelector('kbn-prototype-hardening').getAttribute('data'));
|
||||
window.__kbnHardenPrototypes__ = kbnHardenPrototypes.hardenPrototypes;
|
||||
window.__kbnStrictCsp__ = kbnCsp.strictCsp;
|
||||
window.__kbnThemeTag__ = \\"v8light\\";
|
||||
window.__kbnPublicPath__ = {\\"foo\\": \\"bar\\"};
|
||||
|
|
|
@ -49,6 +49,8 @@ function kbnBundlesLoader() {
|
|||
}
|
||||
|
||||
var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data'));
|
||||
var kbnHardenPrototypes = JSON.parse(document.querySelector('kbn-prototype-hardening').getAttribute('data'));
|
||||
window.__kbnHardenPrototypes__ = kbnHardenPrototypes.hardenPrototypes;
|
||||
window.__kbnStrictCsp__ = kbnCsp.strictCsp;
|
||||
window.__kbnThemeTag__ = "${themeTag}";
|
||||
window.__kbnPublicPath__ = ${publicPathMap};
|
||||
|
|
|
@ -240,6 +240,7 @@ export class RenderingService {
|
|||
const bootstrapScript = isAnonymousPage ? 'bootstrap-anonymous.js' : 'bootstrap.js';
|
||||
const metadata: RenderingMetadata = {
|
||||
strictCsp: http.csp.strict,
|
||||
hardenPrototypes: http.prototypeHardening,
|
||||
uiPublicUrl: `${staticAssetsHrefBase}/ui`,
|
||||
bootstrapScriptUrl: `${basePath}/${bootstrapScript}`,
|
||||
locale,
|
||||
|
|
|
@ -28,6 +28,7 @@ import type { InternalFeatureFlagsSetup } from '@kbn/core-feature-flags-server-i
|
|||
|
||||
/** @internal */
|
||||
export interface RenderingMetadata {
|
||||
hardenPrototypes: ICspConfig['strict'];
|
||||
strictCsp: ICspConfig['strict'];
|
||||
uiPublicUrl: string;
|
||||
bootstrapScriptUrl: string;
|
||||
|
|
|
@ -28,6 +28,7 @@ export const Template: FunctionComponent<Props> = ({
|
|||
scriptPaths,
|
||||
injectedMetadata,
|
||||
bootstrapScriptUrl,
|
||||
hardenPrototypes,
|
||||
strictCsp,
|
||||
customBranding,
|
||||
},
|
||||
|
@ -70,6 +71,9 @@ export const Template: FunctionComponent<Props> = ({
|
|||
{createElement('kbn-csp', {
|
||||
data: JSON.stringify({ strictCsp }),
|
||||
})}
|
||||
{createElement('kbn-prototype-hardening', {
|
||||
data: JSON.stringify({ hardenPrototypes }),
|
||||
})}
|
||||
{createElement('kbn-injected-metadata', { data: JSON.stringify(injectedMetadata) })}
|
||||
<div
|
||||
className="kbnWelcomeView"
|
||||
|
|
32
packages/kbn-security-hardening/BUILD.bazel
Normal file
32
packages/kbn-security-hardening/BUILD.bazel
Normal file
|
@ -0,0 +1,32 @@
|
|||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
|
||||
|
||||
SRCS = glob(
|
||||
[
|
||||
"**/*.ts",
|
||||
],
|
||||
exclude = [
|
||||
"**/test_helpers.ts",
|
||||
"**/*.config.js",
|
||||
"**/*.mock.*",
|
||||
"**/*.test.*",
|
||||
"**/*.stories.*",
|
||||
"**/__snapshots__/**",
|
||||
"**/integration_tests/**",
|
||||
"**/mocks/**",
|
||||
"**/scripts/**",
|
||||
"**/storybook/**",
|
||||
"**/test_fixtures/**",
|
||||
"**/test_helpers/**",
|
||||
],
|
||||
)
|
||||
|
||||
BUNDLER_DEPS = [
|
||||
]
|
||||
|
||||
js_library(
|
||||
name = "kbn-security-hardening",
|
||||
package_name = "@kbn/security-hardening",
|
||||
srcs = ["package.json"] + SRCS,
|
||||
deps = BUNDLER_DEPS,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
|
@ -4,4 +4,8 @@ A package counterpart of `src/setup_node_env/harden` - containing overrides, uti
|
|||
|
||||
## console
|
||||
|
||||
When running in production mode (`process.env.NODE_ENV === 'production'`), global console methods `debug`, `error`, `info`, `log`, `trace`, and `warn` are overridden to implement input sanitization. The export `unsafeConsole` provides access to the unmodified global console methods.
|
||||
When running in production mode (`process.env.NODE_ENV === 'production'`), global console methods `debug`, `error`, `info`, `log`, `trace`, and `warn` are overridden to implement input sanitization. The export `unsafeConsole` provides access to the unmodified global console methods.
|
||||
|
||||
## prototype
|
||||
|
||||
The prototypes of most built-in classes are sealed to mitigate many prototype pollution vulnerabilities.
|
|
@ -7,4 +7,5 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import './prototype';
|
||||
export { unsafeConsole } from './console';
|
||||
|
|
44
packages/kbn-security-hardening/prototype.ts
Normal file
44
packages/kbn-security-hardening/prototype.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
/**
|
||||
* Harden the prototypes of built-in objects to prevent prototype pollution attacks.
|
||||
* This function should be called after the polyfills have been loaded, as some polyfills require the prototypes to be mutable.
|
||||
* The one known requirement is corejs mutating the Array prototype.
|
||||
*/
|
||||
function hardenPrototypesPostPolyfill() {
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal
|
||||
// > The Object.seal() static method seals an object.
|
||||
// > Sealing an object prevents extensions and makes existing properties non-configurable.
|
||||
// > A sealed object has a fixed set of properties: new properties cannot be added, existing properties cannot be removed,
|
||||
// > their enumerability and configurability cannot be changed, and its prototype cannot be re-assigned.
|
||||
// > Values of existing properties can still be changed as long as they are writable.
|
||||
// Object.freeze would take this one step further, and prevent the values of the properties from being changed as well.
|
||||
// This is not currently feasible for Kibana, as this functionality is required for some of the libraries that we use, such as react-dom/server.
|
||||
// While Object.seal() is not a silver bullet, it does provide a good balance between security and compatibility.
|
||||
// The goal is to prevent a majority of prototype pollution vulnerabilities that can be exploited by an attacker.
|
||||
|
||||
// ** IMPORTANT **
|
||||
// This is used both in the browser and in Node.js.
|
||||
// For Node.js, we _additionally_ seal most prototypes in `src/setup_node_env/harden/prototype.js`.
|
||||
// This results in sealing most prototypes twice on the server, with the exception of `Array.prototype`, which is only sealed here.
|
||||
// The extra seal is a no-op, but it is done to ensure that the same code is run in both environments.
|
||||
|
||||
Object.seal(Object.prototype);
|
||||
Object.seal(Number.prototype);
|
||||
Object.seal(String.prototype);
|
||||
Object.seal(Function.prototype);
|
||||
Object.seal(Array.prototype);
|
||||
}
|
||||
|
||||
// Use of the `KBN_UNSAFE_DISABLE_PROTOTYPE_HARDENING` environment variable is discouraged, and should only be set to facilitate testing
|
||||
// specific scenarios. This should never be set in production.
|
||||
if (!process.env.KBN_UNSAFE_DISABLE_PROTOTYPE_HARDENING) {
|
||||
hardenPrototypesPostPolyfill();
|
||||
}
|
|
@ -31,6 +31,7 @@ webpack_cli(
|
|||
"//packages/kbn-crypto-browser",
|
||||
"//packages/kbn-es-query",
|
||||
"//packages/kbn-search-errors",
|
||||
"//packages/kbn-security-hardening",
|
||||
"//packages/kbn-std",
|
||||
"//packages/kbn-safer-lodash-set",
|
||||
"//packages/kbn-peggy",
|
||||
|
|
|
@ -8,6 +8,15 @@
|
|||
*/
|
||||
|
||||
require('./polyfills');
|
||||
// Optional prototype hardening. This must occur immediately after polyfills.
|
||||
if (typeof window.__kbnHardenPrototypes__ !== 'boolean') {
|
||||
throw new Error(
|
||||
'Invariant bootstrap failure: __kbnHardenPrototypes__ must be set to true or false'
|
||||
);
|
||||
}
|
||||
if (window.__kbnHardenPrototypes__) {
|
||||
require('@kbn/security-hardening/prototype');
|
||||
}
|
||||
|
||||
export const Jquery = require('jquery');
|
||||
window.$ = window.jQuery = Jquery;
|
||||
|
|
|
@ -126,6 +126,8 @@ export function applyConfigOverrides(rawConfig, opts, extraCliOptions, keystoreC
|
|||
);
|
||||
}
|
||||
|
||||
set('server.prototypeHardening', true);
|
||||
|
||||
if (!has('elasticsearch.serviceAccountToken') && opts.devCredentials !== false) {
|
||||
if (!has('elasticsearch.username')) {
|
||||
set('elasticsearch.username', 'kibana_system');
|
||||
|
|
|
@ -165,6 +165,7 @@ kibana_vars=(
|
|||
server.name
|
||||
server.port
|
||||
server.protocol
|
||||
server.prototypeHardening
|
||||
server.publicBaseUrl
|
||||
server.requestId.allowFromAnyIp
|
||||
server.requestId.ipAllowlist
|
||||
|
|
3
src/setup_node_env/harden/prototype.js
vendored
3
src/setup_node_env/harden/prototype.js
vendored
|
@ -24,7 +24,8 @@ function hardenPrototypes() {
|
|||
Object.seal(String.prototype);
|
||||
Object.seal(Function.prototype);
|
||||
|
||||
// corejs currently manipulates Array.prototype, so we cannot seal it.
|
||||
// corejs currently manipulates Array.prototype, so we cannot seal it here.
|
||||
// this is instead sealed within `packages/kbn-security-hardening/prototype.ts`
|
||||
}
|
||||
|
||||
module.exports = hardenPrototypes;
|
||||
|
|
|
@ -29,6 +29,7 @@ export default function () {
|
|||
sourceArgs: ['--no-base-path', '--env.name=development'],
|
||||
serverArgs: [
|
||||
`--server.port=${kbnTestConfig.getPort()}`,
|
||||
`--server.prototypeHardening=true`,
|
||||
'--status.allowAnonymous=true',
|
||||
// We shouldn't embed credentials into the URL since Kibana requests to Elasticsearch should
|
||||
// either include `kibanaServerTestUser` credentials, or credentials provided by the test
|
||||
|
|
85
test/plugin_functional/plugins/hardening/common/pollute.ts
Normal file
85
test/plugin_functional/plugins/hardening/common/pollute.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export interface PollutionResult {
|
||||
object: { prototype?: Record<any, any>; error?: string };
|
||||
number: { prototype?: Record<any, any>; error?: string };
|
||||
string: { prototype?: Record<any, any>; error?: string };
|
||||
boolean: { prototype?: Record<any, any>; error?: string };
|
||||
fn: { prototype?: Record<any, any>; error?: string };
|
||||
array: { prototype?: Record<any, any>; error?: string };
|
||||
}
|
||||
|
||||
export function tryPollutingPrototypes(): PollutionResult {
|
||||
const result: PollutionResult = {
|
||||
object: {},
|
||||
number: {},
|
||||
string: {},
|
||||
boolean: {},
|
||||
fn: {},
|
||||
array: {},
|
||||
};
|
||||
|
||||
// Attempt to pollute Object.prototype
|
||||
try {
|
||||
(({}) as any).__proto__.polluted = true;
|
||||
} catch (e) {
|
||||
result.object.error = e.message;
|
||||
} finally {
|
||||
result.object.prototype = { ...Object.keys(Object.getPrototypeOf({})) };
|
||||
}
|
||||
|
||||
// Attempt to pollute String.prototype
|
||||
try {
|
||||
('asdf' as any).__proto__.polluted = true;
|
||||
} catch (e) {
|
||||
result.string.error = e.message;
|
||||
} finally {
|
||||
result.string.prototype = { ...Object.keys(Object.getPrototypeOf('asf')) };
|
||||
}
|
||||
|
||||
// Attempt to pollute Number.prototype
|
||||
try {
|
||||
(12 as any).__proto__.polluted = true;
|
||||
} catch (e) {
|
||||
result.number.error = e.message;
|
||||
} finally {
|
||||
result.number.prototype = { ...Object.keys(Object.getPrototypeOf(12)) };
|
||||
}
|
||||
|
||||
// Attempt to pollute Boolean.prototype
|
||||
try {
|
||||
(true as any).__proto__.polluted = true;
|
||||
} catch (e) {
|
||||
result.boolean.error = e.message;
|
||||
} finally {
|
||||
result.boolean.prototype = { ...Object.keys(Object.getPrototypeOf(true)) };
|
||||
}
|
||||
|
||||
// Attempt to pollute Function.prototype
|
||||
const fn = function fn() {};
|
||||
try {
|
||||
(fn as any).__proto__.polluted = true;
|
||||
} catch (e) {
|
||||
result.fn.error = e.message;
|
||||
} finally {
|
||||
result.fn.prototype = { ...Object.keys(Object.getPrototypeOf(fn)) };
|
||||
}
|
||||
|
||||
// Attempt to pollute Array.prototype
|
||||
try {
|
||||
([] as any).__proto__.polluted = true;
|
||||
} catch (e) {
|
||||
result.array.error = e.message;
|
||||
} finally {
|
||||
result.array.prototype = { ...Object.keys(Object.getPrototypeOf([])) };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
"plugin": {
|
||||
"id": "hardeningPlugin",
|
||||
"server": true,
|
||||
"browser": false,
|
||||
"browser": true,
|
||||
"configPath": [
|
||||
"hardening_plugin"
|
||||
]
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiPageTemplate, EuiTitle, EuiText, EuiProvider } from '@elastic/eui';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { AppMountParameters, CoreStart } from '@kbn/core/public';
|
||||
import { tryPollutingPrototypes } from '../common/pollute';
|
||||
|
||||
export const renderApp = (_core: CoreStart, { element }: AppMountParameters) => {
|
||||
const result = JSON.stringify(tryPollutingPrototypes(), null, 2);
|
||||
|
||||
ReactDOM.render(
|
||||
<EuiProvider>
|
||||
<EuiPageTemplate restrictWidth="1000px">
|
||||
<EuiPageTemplate.Header>
|
||||
<EuiTitle size="l">
|
||||
<h1>Hardening tests</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageTemplate.Header>
|
||||
<EuiPageTemplate.Section>
|
||||
<EuiTitle>
|
||||
<h2>Goal of this page</h2>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>
|
||||
The goal of this page is to attempt to pollute prototypes client-side, and report on
|
||||
the success/failure of these attempts.
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiPageTemplate.Section>
|
||||
<EuiPageTemplate.Section>
|
||||
<EuiTitle>
|
||||
<h2>Result</h2>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<pre data-test-subj="pollution-result">{result}</pre>
|
||||
</EuiText>
|
||||
</EuiPageTemplate.Section>
|
||||
</EuiPageTemplate>
|
||||
</EuiProvider>,
|
||||
element
|
||||
);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
14
test/plugin_functional/plugins/hardening/public/index.ts
Normal file
14
test/plugin_functional/plugins/hardening/public/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { HardeningPlugin } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new HardeningPlugin();
|
||||
}
|
34
test/plugin_functional/plugins/hardening/public/plugin.ts
Normal file
34
test/plugin_functional/plugins/hardening/public/plugin.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { AppMountParameters, CoreSetup, Plugin } from '@kbn/core/public';
|
||||
|
||||
export class HardeningPlugin implements Plugin<HardeningPluginSetup, HardeningPluginStart> {
|
||||
public setup(core: CoreSetup) {
|
||||
core.application.register({
|
||||
id: 'hardeningPlugin',
|
||||
title: 'Hardening Plugin',
|
||||
async mount(params: AppMountParameters) {
|
||||
const { renderApp } = await import('./application');
|
||||
const [coreStart] = await core.getStartServices();
|
||||
coreStart.chrome.docTitle.change('Hardening test');
|
||||
return renderApp(coreStart, params);
|
||||
},
|
||||
});
|
||||
|
||||
// Return methods that should be available to other plugins
|
||||
return {};
|
||||
}
|
||||
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
||||
|
||||
export type HardeningPluginSetup = ReturnType<HardeningPlugin['setup']>;
|
||||
export type HardeningPluginStart = ReturnType<HardeningPlugin['start']>;
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
import type { Plugin, CoreSetup } from '@kbn/core/server';
|
||||
import { tryPollutingPrototypes } from '../common/pollute';
|
||||
|
||||
export class HardeningPlugin implements Plugin {
|
||||
public setup(core: CoreSetup, deps: {}) {
|
||||
|
@ -17,59 +18,7 @@ export class HardeningPlugin implements Plugin {
|
|||
validate: false,
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const result: Record<string, { prototype?: Record<any, any>; error?: string }> = {
|
||||
object: {},
|
||||
number: {},
|
||||
string: {},
|
||||
fn: {},
|
||||
array: {},
|
||||
};
|
||||
// Attempt to pollute Object.prototype
|
||||
try {
|
||||
(({}) as any).__proto__.polluted = true;
|
||||
} catch (e) {
|
||||
result.object.error = e.message;
|
||||
} finally {
|
||||
result.object.prototype = { ...Object.keys(Object.getPrototypeOf({})) };
|
||||
}
|
||||
|
||||
// Attempt to pollute String.prototype
|
||||
try {
|
||||
('asdf' as any).__proto__.polluted = true;
|
||||
} catch (e) {
|
||||
result.string.error = e.message;
|
||||
} finally {
|
||||
result.string.prototype = { ...Object.keys(Object.getPrototypeOf('asf')) };
|
||||
}
|
||||
|
||||
// Attempt to pollute Number.prototype
|
||||
try {
|
||||
(12 as any).__proto__.polluted = true;
|
||||
} catch (e) {
|
||||
result.number.error = e.message;
|
||||
} finally {
|
||||
result.number.prototype = { ...Object.keys(Object.getPrototypeOf(12)) };
|
||||
}
|
||||
|
||||
// Attempt to pollute Function.prototype
|
||||
const fn = function fn() {};
|
||||
try {
|
||||
(fn as any).__proto__.polluted = true;
|
||||
} catch (e) {
|
||||
result.fn.error = e.message;
|
||||
} finally {
|
||||
result.fn.prototype = { ...Object.keys(Object.getPrototypeOf(fn)) };
|
||||
}
|
||||
|
||||
// Attempt to pollute Array.prototype
|
||||
try {
|
||||
([] as any).__proto__.polluted = true;
|
||||
} catch (e) {
|
||||
result.array.error = e.message;
|
||||
} finally {
|
||||
result.array.prototype = { ...Object.keys(Object.getPrototypeOf([])) };
|
||||
}
|
||||
|
||||
const result = tryPollutingPrototypes();
|
||||
return response.ok({ body: result });
|
||||
}
|
||||
);
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"common/**/*.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"server/**/*.ts",
|
||||
"../../../../typings/**/*",
|
||||
],
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"object":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"number":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"string":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"fn":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"array":{"prototype":{"0":"polluted"}}}
|
||||
{"object":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"number":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"string":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"boolean":{"prototype":{"0":"polluted"}},"fn":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"array":{"error":"Cannot add property polluted, object is not extensible","prototype":{}}}
|
|
@ -0,0 +1 @@
|
|||
{"object":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"number":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"string":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"boolean":{"prototype":{"0":"polluted"}},"fn":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"array":{"error":"Cannot add property polluted, object is not extensible","prototype":{}}}
|
|
@ -0,0 +1 @@
|
|||
{"object":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"number":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"string":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"boolean":{"prototype":{"0":"polluted"}},"fn":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"array":{"error":"Cannot add property polluted, object is not extensible","prototype":{}}}
|
|
@ -0,0 +1 @@
|
|||
{"object":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"number":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"string":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"boolean":{"prototype":{"0":"polluted"}},"fn":{"error":"Cannot add property polluted, object is not extensible","prototype":{}},"array":{"error":"Cannot add property polluted, object is not extensible","prototype":{}}}
|
|
@ -7,20 +7,35 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import type { PluginFunctionalProviderContext } from '../../services';
|
||||
|
||||
export default function ({ getService }: PluginFunctionalProviderContext) {
|
||||
export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) {
|
||||
const PageObjects = getPageObjects(['common', 'header']);
|
||||
const browser = getService('browser');
|
||||
const supertest = getService('supertest');
|
||||
const snapshots = getService('snapshots');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
describe('prototype', function () {
|
||||
it('does not allow polluting most prototypes', async () => {
|
||||
it('does not allow polluting most prototypes on the server', async () => {
|
||||
const response = await supertest
|
||||
.get('/api/hardening/_pollute_prototypes')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
|
||||
await snapshots.compareAgainstBaseline('hardening/prototype', response.body);
|
||||
await snapshots.compareAgainstBaseline('hardening/prototype_server', response.body);
|
||||
});
|
||||
|
||||
it('does not allow polluting most prototypes on the client', async () => {
|
||||
const pageTitle = 'Hardening test - Elastic';
|
||||
|
||||
await PageObjects.common.navigateToApp('hardeningPlugin');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
expect(await browser.getTitle()).eql(pageTitle);
|
||||
|
||||
const resultText = await testSubjects.getVisibleText('pollution-result');
|
||||
await snapshots.compareAgainstBaseline('hardening/prototype_client', JSON.parse(resultText));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React, { FC, PropsWithChildren } from 'react';
|
||||
// @ts-expect-error untyped library
|
||||
import Dropzone from 'react-dropzone';
|
||||
|
||||
import './upload_dropzone.scss';
|
||||
|
@ -21,14 +20,23 @@ export const UploadDropzone: FC<PropsWithChildren<Props>> = ({
|
|||
disabled,
|
||||
children,
|
||||
}) => {
|
||||
const dropFn = (acceptedFiles: File[]) => {
|
||||
const fileList = acceptedFiles as unknown as FileList;
|
||||
onDrop(fileList);
|
||||
};
|
||||
return (
|
||||
<Dropzone
|
||||
{...{ onDrop, disabled }}
|
||||
disableClick
|
||||
className="canvasWorkpad__dropzone"
|
||||
activeClassName="canvasWorkpad__dropzone--active"
|
||||
>
|
||||
{children}
|
||||
<Dropzone {...{ onDrop: dropFn, disabled }} noClick>
|
||||
{({ getRootProps, isDragActive }) => (
|
||||
<div
|
||||
{...getRootProps({
|
||||
className: `canvasWorkpad__dropzone${
|
||||
isDragActive ? ' canvasWorkpad__dropzone--active' : ''
|
||||
}`,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -71,9 +71,9 @@ jest.mock('@kbn/presentation-util-plugin/public/components/expression_input');
|
|||
// @ts-expect-error
|
||||
ExpressionInput.mockImplementation(() => 'ExpressionInput');
|
||||
|
||||
// @ts-expect-error untyped library
|
||||
import Dropzone from 'react-dropzone';
|
||||
jest.mock('react-dropzone');
|
||||
// @ts-expect-error untyped library
|
||||
Dropzone.mockImplementation(() => 'Dropzone');
|
||||
|
||||
// This element uses a `ref` and cannot be rendered by Jest snapshots.
|
||||
|
|
|
@ -111,6 +111,7 @@ export default async () => {
|
|||
serverArgs: [
|
||||
`--server.restrictInternalApis=true`,
|
||||
`--server.port=${servers.kibana.port}`,
|
||||
`--server.prototypeHardening=true`,
|
||||
'--status.allowAnonymous=true',
|
||||
`--migrations.zdt.runOnRoles=${JSON.stringify(['ui'])}`,
|
||||
// We shouldn't embed credentials into the URL since Kibana requests to Elasticsearch should
|
||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -13986,13 +13986,6 @@ atomic-sleep@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
|
||||
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
|
||||
|
||||
attr-accept@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52"
|
||||
integrity sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==
|
||||
dependencies:
|
||||
core-js "^2.5.0"
|
||||
|
||||
attr-accept@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
|
||||
|
@ -27588,14 +27581,6 @@ react-dropzone@^11.7.1:
|
|||
file-selector "^0.4.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-dropzone@^4.2.9:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-4.3.0.tgz#facdd7db16509772633c9f5200621ac01aa6706f"
|
||||
integrity sha512-ULfrLaTSsd8BDa9KVAGCueuq1AN3L14dtMsGGqtP0UwYyjG4Vhf158f/ITSHuSPYkZXbvfcIiOlZsH+e3QWm+Q==
|
||||
dependencies:
|
||||
attr-accept "^1.1.3"
|
||||
prop-types "^15.5.7"
|
||||
|
||||
react-element-to-jsx-string@^14.3.4:
|
||||
version "14.3.4"
|
||||
resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.4.tgz#709125bc72f06800b68f9f4db485f2c7d31218a8"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue