[8.17] Additional prototype pollution protections (#206073) (#216286)

# 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:
Sid 2025-04-04 20:52:26 +02:00 committed by GitHub
parent 7746e727fc
commit bbb6a64691
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 361 additions and 85 deletions

View file

@ -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",

View file

@ -84,6 +84,7 @@ Object {
"payloadTimeout": 20000,
"port": 5601,
"protocol": "http1",
"prototypeHardening": false,
"requestId": Object {
"allowFromAnyIp": false,
"ipAllowlist": Array [],

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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),

View file

@ -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

View file

@ -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(),

View file

@ -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\\"};

View file

@ -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};

View file

@ -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,

View file

@ -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;

View file

@ -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"

View 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"],
)

View file

@ -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.

View file

@ -7,4 +7,5 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import './prototype';
export { unsafeConsole } from './console';

View 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();
}

View file

@ -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",

View file

@ -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;

View file

@ -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');

View file

@ -165,6 +165,7 @@ kibana_vars=(
server.name
server.port
server.protocol
server.prototypeHardening
server.publicBaseUrl
server.requestId.allowFromAnyIp
server.requestId.ipAllowlist

View file

@ -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;

View file

@ -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

View 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;
}

View file

@ -5,7 +5,7 @@
"plugin": {
"id": "hardeningPlugin",
"server": true,
"browser": false,
"browser": true,
"configPath": [
"hardening_plugin"
]

View file

@ -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);
};

View 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();
}

View 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']>;

View file

@ -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 });
}
);

View file

@ -5,6 +5,9 @@
},
"include": [
"index.ts",
"common/**/*.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../../../typings/**/*",
],

View file

@ -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":{}}}

View file

@ -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":{}}}

View file

@ -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":{}}}

View file

@ -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":{}}}

View file

@ -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));
});
});
}

View file

@ -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>
);
};

View file

@ -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.

View file

@ -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

View file

@ -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"