Skip to content

Commit 627c8fd

Browse files
committed
Run jest tests in a simulated disallowed environment
1 parent 75c2800 commit 627c8fd

4 files changed

Lines changed: 118 additions & 0 deletions

File tree

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: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 = [/jest-environment/, /jest-mock/, /jest-runtime/, /jest-snapshot/];
46+
47+
function isCallerAllowed() {
48+
const stack = new Error().stack || '';
49+
return ALLOWED_CALLERS.some((pattern) => pattern.test(stack));
50+
}
51+
52+
// eslint-disable-next-line no-eval -- intentionally replacing eval to block code generation
53+
global.eval = function () {
54+
throw new EvalError(ERROR_MESSAGE);
55+
};
56+
57+
const OriginalFunction = global.Function;
58+
const FunctionProxy = new Proxy(OriginalFunction, {
59+
apply(target, thisArg, args) {
60+
if (isCallerAllowed()) {
61+
return Reflect.apply(target, thisArg, args);
62+
}
63+
throw new EvalError(ERROR_MESSAGE);
64+
},
65+
construct(target, args, newTarget) {
66+
if (isCallerAllowed()) {
67+
return Reflect.construct(target, args, newTarget);
68+
}
69+
throw new EvalError(ERROR_MESSAGE);
70+
},
71+
});
72+
73+
Object.defineProperty(FunctionProxy, 'prototype', {
74+
value: OriginalFunction.prototype,
75+
writable: false,
76+
});
77+
78+
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)