Skip to content
Open
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
1 change: 1 addition & 0 deletions .buildkite/scout_ci_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ packages:
enabled:
- core
- kbn-scout
- kbn-security-hardening
- kbn-streamlang-tests
- user-storage
disabled:
Expand Down
2 changes: 2 additions & 0 deletions .buildkite/scripts/common/env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export NODE_OPTIONS="--max-old-space-size=4096"
export FORCE_COLOR=1
export TEST_BROWSER_HEADLESS=1

export KBN_DISALLOW_CODE_GEN_FROM_STRINGS=true

export ELASTIC_APM_ENVIRONMENT=ci
export ELASTIC_APM_TRANSACTION_SAMPLE_RATE=0.01
export ELASTIC_APM_KIBANA_FRONTEND_ACTIVE=false
Expand Down
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -2430,6 +2430,7 @@ x-pack/platform/plugins/shared/inference_endpoint @elastic/search-kibana

# Kibana Platform Security
packages/kbn-eslint-plugin-eslint/rules/no_unsafe_hash.js @elastic/kibana-security
/src/platform/packages/shared/kbn-test/src/jest/setup/disallow_code_generation.js @elastic/kibana-security
/src/setup_node_env/harden @elastic/kibana-security

/x-pack/platform/test/fixtures/es_archives/security @elastic/kibana-security
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-cli-dev-mode/src/dev_server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ describe('#run$', () => {
"--inheritted",
"--exec",
"--argv",
"--disallow-code-generation-from-strings",
],
"stdio": "pipe",
},
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-cli-dev-mode/src/using_server_process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function usingServerProcess<T>(
nodeOptions: [
...process.execArgv,
...(ACTIVE_INSPECT_FLAG ? [`${ACTIVE_INSPECT_FLAG}=${process.debugPort + 1}`] : []),
'--disallow-code-generation-from-strings',
],
env: {
...process.env,
Expand Down
3 changes: 3 additions & 0 deletions src/dev/build/tasks/bin/scripts/kibana
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ NODE="${DIR}/node/pointer-compression/bin/node"
test_node

BASE_NODE_OPTIONS="--no-warnings --max-http-header-size=65536"
if [ "$KBN_DISALLOW_CODE_GEN_FROM_STRINGS" = 'true' ]; then
BASE_NODE_OPTIONS="$BASE_NODE_OPTIONS --disallow-code-generation-from-strings"
fi
if [ -f "${CONFIG_DIR}/node.options" ]; then
KBN_NODE_OPTS="$(grep -v ^# < ${CONFIG_DIR}/node.options | xargs)"
fi
Expand Down
3 changes: 3 additions & 0 deletions src/dev/build/tasks/bin/scripts/kibana.bat
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ IF EXIST "%CONFIG_DIR%\node.options" (

:: Include pre-defined node option
set "NODE_OPTIONS=--no-warnings --max-http-header-size=65536 %NODE_OPTIONS%"
IF "%KBN_DISALLOW_CODE_GEN_FROM_STRINGS%"=="true" (
set "NODE_OPTIONS=--disallow-code-generation-from-strings %NODE_OPTIONS%"
)

:: This should run independently as the last instruction
:: as we need NODE_OPTIONS previously set to expand
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 { resolve } from 'path';
import { REPO_ROOT } from '@kbn/repo-info';
import type { ScoutServerConfig } from '../../../../../types';
import { defaultConfig } from '../../default/stateful/base.config';

const pluginPath = `--plugin-path=${resolve(
REPO_ROOT,
'src/platform/test/plugin_functional/plugins/hardening'
)}`;

export const servers: ScoutServerConfig = {
...defaultConfig,
kbnTestServer: {
...defaultConfig.kbnTestServer,
serverArgs: [...defaultConfig.kbnTestServer.serverArgs, pluginPath],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ When running in production mode (`process.env.NODE_ENV === 'production'`), globa

## prototype

The prototypes of most built-in classes are sealed to mitigate many prototype pollution vulnerabilities.
The prototypes of most built-in classes are sealed to mitigate many prototype pollution vulnerabilities.

## Testing

Scout API tests live in `test/scout_hardening/api/` and verify that server-side hardening measures are active at runtime (e.g. `--disallow-code-generation-from-strings` blocking `eval()` and `new Function()`). These tests require the `hardening` server config set which loads the test plugin from `src/platform/test/plugin_functional/plugins/hardening/`.
3 changes: 2 additions & 1 deletion src/platform/packages/shared/kbn-security-hardening/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ project:
channel: ''
owner: '@elastic/kibana-security'
sourceRoot: src/platform/packages/shared/kbn-security-hardening
dependsOn: []
dependsOn:
- '@kbn/scout'
tags:
- shared-common
- package
Expand Down
Original file line number Diff line number Diff line change
@@ -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 { createPlaywrightConfig } from '@kbn/scout';

export default createPlaywrightConfig({
testDir: './tests',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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 type { RoleApiCredentials } from '@kbn/scout';
import { apiTest } from '@kbn/scout';
import { expect } from '@kbn/scout/api';

apiTest.describe(
'code generation from strings is disallowed',
{ tag: ['@local-stateful-classic'] },
() => {
let credentials: RoleApiCredentials;

apiTest.beforeAll(async ({ requestAuth }) => {
credentials = await requestAuth.getApiKey('viewer');
});

apiTest('eval is blocked on the server', async ({ apiClient }) => {
const response = await apiClient.get('/internal/hardening/_try_code_generation', {
headers: {
...credentials.apiKeyHeader,
'x-elastic-internal-origin': 'kibana',
},
});

expect(response).toHaveStatusCode(200);
expect(response.body.eval.blocked).toBe(true);
expect(response.body.eval.error).toContain(
'Code generation from strings disallowed for this context'
);
});

apiTest('Function constructor is blocked on the server', async ({ apiClient }) => {
const response = await apiClient.get('/internal/hardening/_try_code_generation', {
headers: {
...credentials.apiKeyHeader,
'x-elastic-internal-origin': 'kibana',
},
});

expect(response).toHaveStatusCode(200);
expect(response.body.functionConstructor.blocked).toBe(true);
expect(response.body.functionConstructor.error).toContain(
'Code generation from strings disallowed for this context'
);
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@
"exclude": [
"target/**/*"
],
"kbn_references": []
"kbn_references": [
"@kbn/scout",
]
}
1 change: 1 addition & 0 deletions src/platform/packages/shared/kbn-test/jest-preset.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ module.exports = {

// A list of paths to modules that run some code to configure or set up the testing framework before each test
setupFilesAfterEnv: [
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/disallow_code_generation.js',
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/setup_test.js',
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/mocks.moment_timezone.js',
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/mocks.eui.js',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ module.exports = {
'[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|@langchain|zod/v4))/dist/util/[/\\\\].+\\.js$',
],
setupFilesAfterEnv: [
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/disallow_code_generation.js',
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/after_env.integration.js',
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/mocks.moment_timezone.js',
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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".
*/

/*
* Disallow runtime code generation from strings in Jest tests.
*
* In production and dev mode, Kibana runs with the V8 flag
* --disallow-code-generation-from-strings, which causes eval() and
* new Function(string) to throw:
*
* "Code generation from strings disallowed for this context"
*
* We cannot use that V8 flag for Jest because Jest's own dependencies
* (tmpl -> makeerror -> walker) call new Function() during startup.
* Instead, this setup file replaces eval and Function with versions that
* throw the same EvalError, scoped to the test sandbox only.
*
* Jest's own mock system (jest-mock) uses new Function() to create mock
* functions that preserve the original function's name. These calls are
* exempted by checking the call stack for known Jest internal callers.
*
* If your test is failing with this error, it means the code under test
* uses eval() or new Function() — which would also fail in production.
* Fix the underlying code rather than removing this restriction.
*
* See: packages/kbn-cli-dev-mode/src/using_server_process.ts (dev flag)
* src/dev/build/tasks/bin/scripts/kibana (prod flag)
* src/platform/packages/shared/kbn-security-hardening/ (hardening package)
*/

const ERROR_MESSAGE = 'Code generation from strings disallowed for this context';

// Zod v4 has a JIT compiler that uses new Function() for schema parsing.
// Setting jitless on the backing store ensures Zod skips JIT regardless of
// import order. Zod's own allowsEval probe is unreliable here because module
// caching can cause it to run before our Function proxy is installed.
global.__zod_globalConfig = Object.assign(global.__zod_globalConfig || {}, { jitless: true });

const ALLOWED_CALLERS = [
// ESLint's ajv plugin uses new Function() for schema parsing. Dev-only, this is OK.
/eslint.*ajv/,
// i18n_eui_mapping.test.ts intentionally uses eval within its harness. Dev-only, this is OK.
/i18n_eui_mapping.*\.test/,
// Jest's own mock system (jest-mock) uses new Function() to create mock. Dev-only, this is OK.
/jest-mock/,
// Jest's own runtime uses new Function() for code generation. Dev-only, this is OK.
/jest-runtime/,
// Jest's own snapshot system uses new Function() for code generation. Dev-only, this is OK.
/jest-snapshot/,
// Jest's own environment uses new Function() for code generation. Dev-only, this is OK.
/jest-environment/,
// kbn-handlebars tests intentionally exercise the eval-based Handlebars compiler
// to verify parity with the safe AST-based replacement. The CSP probe
// (kbnUnsafeEvalTest) is blocked separately above, so this exception only
// affects the actual parity-test compilation calls.
/kbn-handlebars.*test/,
// hmr_client.test uses new Function() to load a browser bundle with injected globals;
// the bundle itself does not use code generation in production.
/kbn-rspack-optimizer.*hmr_client\.test/,
];

// @kbn/handlebars probes for CSP unsafe-eval support by calling
// new Function('kbnUnsafeEvalTest', 'return true;'). In Jest the jest-runtime
// allow-list entry would let this probe succeed (jest-runtime is in the stack
// during module loading), causing handlebars to pick the eval-based compiler
// that later fails. Blocking the probe explicitly makes the Jest environment
// match browser CSP behavior, routing to the safe compileAST path.
const CSP_PROBE_MARKER = 'kbnUnsafeEvalTest';

function isCspProbe(args) {
return args.length > 0 && args[0] === CSP_PROBE_MARKER;
}

function isCallerAllowed() {
const stack = new Error().stack || '';
return ALLOWED_CALLERS.some((pattern) => pattern.test(stack));
}

// eslint-disable-next-line no-eval -- intentionally replacing eval to block code generation
const OriginalEval = global.eval;

// eslint-disable-next-line no-eval -- intentionally replacing eval to block code generation
global.eval = function () {
if (isCallerAllowed()) {
return OriginalEval.apply(this, arguments);
}
throw new EvalError(ERROR_MESSAGE);
};

const OriginalFunction = global.Function;
const FunctionProxy = new Proxy(OriginalFunction, {
apply(target, thisArg, args) {
if (!isCspProbe(args) && isCallerAllowed()) {
return Reflect.apply(target, thisArg, args);
}
throw new EvalError(ERROR_MESSAGE);
},
construct(target, args, newTarget) {
if (!isCspProbe(args) && isCallerAllowed()) {
return Reflect.construct(target, args, newTarget);
}
throw new EvalError(ERROR_MESSAGE);
},
});

Object.defineProperty(FunctionProxy, 'prototype', {
value: OriginalFunction.prototype,
writable: false,
});

global.Function = FunctionProxy;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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".
*/

const ERROR_MESSAGE = 'Code generation from strings disallowed for this context';

describe('disallow_code_generation setup', () => {
it('blocks eval()', () => {
// eslint-disable-next-line no-eval -- verifying that eval is blocked
expect(() => eval('1+1')).toThrow(EvalError);
// eslint-disable-next-line no-eval -- verifying that eval is blocked
expect(() => eval('1+1')).toThrow(ERROR_MESSAGE);
});

it('blocks new Function()', () => {
// eslint-disable-next-line no-new-func -- verifying that Function constructor is blocked
expect(() => new Function('return 1+1')).toThrow(EvalError);
// eslint-disable-next-line no-new-func -- verifying that Function constructor is blocked
expect(() => new Function('return 1+1')).toThrow(ERROR_MESSAGE);
});

it('blocks Function() called without new', () => {
// eslint-disable-next-line no-new-func -- verifying that Function() is blocked
expect(() => Function('return 1+1')).toThrow(EvalError);
// eslint-disable-next-line no-new-func -- verifying that Function() is blocked
expect(() => Function('return 1+1')).toThrow(ERROR_MESSAGE);
});

it('preserves Function.prototype so instanceof still works', () => {
const fn = () => {};
expect(fn instanceof Function).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -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".
*/

export interface CodeGenerationResult {
eval: { blocked: boolean; error?: string };
functionConstructor: { blocked: boolean; error?: string };
}

export function tryCodeGeneration(): CodeGenerationResult {
const result: CodeGenerationResult = {
eval: { blocked: false },
functionConstructor: { blocked: false },
};

try {
eval('1+1'); // eslint-disable-line no-eval
} catch (e) {
result.eval = { blocked: true, error: e.message };
}

try {
new Function('return 1+1')(); // eslint-disable-line no-new-func
} catch (e) {
result.functionConstructor = { blocked: true, error: e.message };
}

return result;
}
Loading
Loading