Skip to content

Commit 4fbe88e

Browse files
committed
Run jest tests in a simulated disallowed environment
1 parent a73c2ac commit 4fbe88e

5 files changed

Lines changed: 158 additions & 0 deletions

File tree

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2393,6 +2393,7 @@ x-pack/platform/plugins/shared/inference_endpoint @elastic/search-kibana
23932393

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

23982399
/x-pack/platform/test/fixtures/es_archives/security @elastic/kibana-security

src/platform/packages/shared/kbn-test/jest-preset.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ module.exports = {
7676

7777
// A list of paths to modules that run some code to configure or set up the testing framework before each test
7878
setupFilesAfterEnv: [
79+
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/disallow_code_generation.js',
7980
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/setup_test.js',
8081
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/mocks.moment_timezone.js',
8182
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/mocks.eui.js',

src/platform/packages/shared/kbn-test/jest_integration_node/jest-preset.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ module.exports = {
2828
'[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|@langchain|zod/v4))/dist/util/[/\\\\].+\\.js$',
2929
],
3030
setupFilesAfterEnv: [
31+
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/disallow_code_generation.js',
3132
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/after_env.integration.js',
3233
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/mocks.moment_timezone.js',
3334
],
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
/*
11+
* Disallow runtime code generation from strings in Jest tests.
12+
*
13+
* In production and dev mode, Kibana runs with the V8 flag
14+
* --disallow-code-generation-from-strings, which causes eval() and
15+
* new Function(string) to throw:
16+
*
17+
* "Code generation from strings disallowed for this context"
18+
*
19+
* We cannot use that V8 flag for Jest because Jest's own dependencies
20+
* (tmpl -> makeerror -> walker) call new Function() during startup.
21+
* Instead, this setup file replaces eval and Function with versions that
22+
* throw the same EvalError, scoped to the test sandbox only.
23+
*
24+
* Jest's own mock system (jest-mock) uses new Function() to create mock
25+
* functions that preserve the original function's name. These calls are
26+
* exempted by checking the call stack for known Jest internal callers.
27+
*
28+
* If your test is failing with this error, it means the code under test
29+
* uses eval() or new Function() — which would also fail in production.
30+
* Fix the underlying code rather than removing this restriction.
31+
*
32+
* See: packages/kbn-cli-dev-mode/src/using_server_process.ts (dev flag)
33+
* src/dev/build/tasks/bin/scripts/kibana (prod flag)
34+
* src/platform/packages/shared/kbn-security-hardening/ (hardening package)
35+
*/
36+
37+
const ERROR_MESSAGE = 'Code generation from strings disallowed for this context';
38+
39+
// Zod v4 has a JIT compiler that uses new Function() for schema parsing.
40+
// Setting jitless on the backing store ensures Zod skips JIT regardless of
41+
// import order. Zod's own allowsEval probe is unreliable here because module
42+
// caching can cause it to run before our Function proxy is installed.
43+
global.__zod_globalConfig = Object.assign(global.__zod_globalConfig || {}, { jitless: true });
44+
45+
const ALLOWED_CALLERS = [
46+
// ESLint's ajv plugin uses new Function() for schema parsing. Dev-only, this is OK.
47+
/eslint.*ajv/,
48+
// i18n_eui_mapping.test.ts intentionally uses eval within its harness. Dev-only, this is OK.
49+
/i18n_eui_mapping.*\.test/,
50+
// Jest's own mock system (jest-mock) uses new Function() to create mock. Dev-only, this is OK.
51+
/jest-mock/,
52+
// Jest's own runtime uses new Function() for code generation. Dev-only, this is OK.
53+
/jest-runtime/,
54+
// Jest's own snapshot system uses new Function() for code generation. Dev-only, this is OK.
55+
/jest-snapshot/,
56+
// Jest's own environment uses new Function() for code generation. Dev-only, this is OK.
57+
/jest-environment/,
58+
// kbn-handlebars tests intentionally exercise the eval-based Handlebars compiler
59+
// to verify parity with the safe AST-based replacement. The CSP probe
60+
// (kbnUnsafeEvalTest) is blocked separately above, so this exception only
61+
// affects the actual parity-test compilation calls.
62+
/kbn-handlebars.*test/,
63+
// hmr_client.test uses new Function() to load a browser bundle with injected globals;
64+
// the bundle itself does not use code generation in production.
65+
/kbn-rspack-optimizer.*hmr_client\.test/,
66+
];
67+
68+
// @kbn/handlebars probes for CSP unsafe-eval support by calling
69+
// new Function('kbnUnsafeEvalTest', 'return true;'). In Jest the jest-runtime
70+
// allow-list entry would let this probe succeed (jest-runtime is in the stack
71+
// during module loading), causing handlebars to pick the eval-based compiler
72+
// that later fails. Blocking the probe explicitly makes the Jest environment
73+
// match browser CSP behavior, routing to the safe compileAST path.
74+
const CSP_PROBE_MARKER = 'kbnUnsafeEvalTest';
75+
76+
function isCspProbe(args) {
77+
return args.length > 0 && args[0] === CSP_PROBE_MARKER;
78+
}
79+
80+
function isCallerAllowed() {
81+
const stack = new Error().stack || '';
82+
return ALLOWED_CALLERS.some((pattern) => pattern.test(stack));
83+
}
84+
85+
// eslint-disable-next-line no-eval -- intentionally replacing eval to block code generation
86+
const OriginalEval = global.eval;
87+
88+
// eslint-disable-next-line no-eval -- intentionally replacing eval to block code generation
89+
global.eval = function () {
90+
if (isCallerAllowed()) {
91+
return OriginalEval.apply(this, arguments);
92+
}
93+
throw new EvalError(ERROR_MESSAGE);
94+
};
95+
96+
const OriginalFunction = global.Function;
97+
const FunctionProxy = new Proxy(OriginalFunction, {
98+
apply(target, thisArg, args) {
99+
if (!isCspProbe(args) && isCallerAllowed()) {
100+
return Reflect.apply(target, thisArg, args);
101+
}
102+
throw new EvalError(ERROR_MESSAGE);
103+
},
104+
construct(target, args, newTarget) {
105+
if (!isCspProbe(args) && isCallerAllowed()) {
106+
return Reflect.construct(target, args, newTarget);
107+
}
108+
throw new EvalError(ERROR_MESSAGE);
109+
},
110+
});
111+
112+
Object.defineProperty(FunctionProxy, 'prototype', {
113+
value: OriginalFunction.prototype,
114+
writable: false,
115+
});
116+
117+
global.Function = FunctionProxy;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
const ERROR_MESSAGE = 'Code generation from strings disallowed for this context';
11+
12+
describe('disallow_code_generation setup', () => {
13+
it('blocks eval()', () => {
14+
// eslint-disable-next-line no-eval -- verifying that eval is blocked
15+
expect(() => eval('1+1')).toThrow(EvalError);
16+
// eslint-disable-next-line no-eval -- verifying that eval is blocked
17+
expect(() => eval('1+1')).toThrow(ERROR_MESSAGE);
18+
});
19+
20+
it('blocks new Function()', () => {
21+
// eslint-disable-next-line no-new-func -- verifying that Function constructor is blocked
22+
expect(() => new Function('return 1+1')).toThrow(EvalError);
23+
// eslint-disable-next-line no-new-func -- verifying that Function constructor is blocked
24+
expect(() => new Function('return 1+1')).toThrow(ERROR_MESSAGE);
25+
});
26+
27+
it('blocks Function() called without new', () => {
28+
// eslint-disable-next-line no-new-func -- verifying that Function() is blocked
29+
expect(() => Function('return 1+1')).toThrow(EvalError);
30+
// eslint-disable-next-line no-new-func -- verifying that Function() is blocked
31+
expect(() => Function('return 1+1')).toThrow(ERROR_MESSAGE);
32+
});
33+
34+
it('preserves Function.prototype so instanceof still works', () => {
35+
const fn = () => {};
36+
expect(fn instanceof Function).toBe(true);
37+
});
38+
});

0 commit comments

Comments
 (0)