Code generation from strings

Make eval(), new Function() and every other way a string becomes code throw, for the whole process, with --disallow-code-generation-from-strings.

--disallow-code-generation-from-strings stops a program from turning strings into code. The flag has two levels:

FlagWhat it refuses
--disallow-code-generation-from-stringseval() and the Function constructors. The same as Node.js's flag.
--disallow-code-generation-from-strings=strictEvery way a string becomes code in the process. Bun only.
terminal
bun --disallow-code-generation-from-strings=strict server.ts
server.ts
new Function("return 1 + 1"); // EvalError: Code generation from strings disallowed for this context

A refusal is a synchronous EvalError that you can catch. import() of a refused URL rejects with the same error. A library that probes with try { new Function("") } catch {} takes its fallback path.

Two refusals reach you another way. ShadowRealm.prototype.importValue() of a refused URL rejects with a TypeError whose message contains the EvalError. A refused URL in a Worker's preload option is reported by the Worker's error event.

The flag applies to the whole process#

Bun reads the flag at startup. Nothing turns it off afterwards:

  • There is no API that re-enables code generation.
  • Every Worker has the level of the process, whatever execArgv you give the Worker.
  • Every realm and context the program creates later has it: ShadowRealm, node:vm contexts under =strict, and every Bun.ModuleGraph.
  • The level a compiled executable is built with is a floor. BUN_OPTIONS can raise it and cannot lower it.

When the flag appears more than once on a command line, the last one counts, as for any option. Bun reads BUN_OPTIONS before the command line, so the command line wins.

A Worker whose own execArgv contains the flag throws ERR_WORKER_INVALID_EXEC_ARGV, as in Node.js, whether or not the process has the flag. Pass the flag to the process. new Worker(file, { execArgv: process.execArgv }) throws in a process that has the flag, so leave the flag out of the list you pass on.

Workers whose code is a string#

new Worker(code, { eval: true }) from node:worker_threads takes the Worker's source as a string. So does a Worker started from a data: or blob: URL.

  • With the flag and no value, the Worker starts, as in Node.js. eval() and new Function() throw inside it.
  • With =strict, the Worker constructor throws the EvalError. Put the Worker's code in a file and pass the file's path or URL.

What each level refuses#

A string becomes code throughFlag=strict
eval(), direct and indirectrefusedrefused
new Function(), and the async, generator and async generator function constructors, reached any wayrefusedrefused
ShadowRealm.prototype.evaluate(), and eval() inside a ShadowRealmrefusedrefused
new vm.Script(), vm.runInThisContext(), vm.runInContext(), vm.runInNewContext()allowedrefused
vm.compileFunction(), new vm.SourceTextModule()allowedrefused
eval() inside a node:vm context, including one made with codeGeneration: { strings: true }allowedrefused
module._compile(), and a require.extensions handler that calls itallowedrefused
Assigning a different Module.wrapperallowedrefused
import, import() and require() of a data: or blob: URLallowedrefused
A Worker started from a data: or blob: URL, or with eval: trueallowedrefused
A plugin that returns contents for the js, jsx, ts or tsx loaderallowedrefused
inspector.open() from node:inspector, startRemoteDebugger() from bun:jscallowedrefused
napi_run_script() in a native addonallowedrefused

With the flag and no value, Bun matches Node.js: node:vm is not affected, and a node:vm context decides for itself with its codeGeneration option.

These keep working at both levels:

  • Importing module files, with literal or computed specifiers, and modules embedded in a compiled executable.
  • new Worker() with a file.
  • require() and createRequire() of files and built-in modules.
  • Bun.ModuleGraph.
  • JSON.parse(), RegExp, and eval() of a value that is not a string.
  • WebAssembly.
  • Native addons and bun:ffi. See What the flag does not cover.
  • A plugin that returns an exports object, or contents for a data loader such as json or toml.
  • vm.createContext() and new vm.SyntheticModule(). Neither one compiles a string.

The inspector#

The debugger evaluates the code its client sends, so =strict refuses it. Bun exits with an error at startup when you also pass --inspect, --inspect-wait or --inspect-brk, or when BUN_INSPECT is set:

error: BUN_INSPECT cannot be used with --disallow-code-generation-from-strings=strict: the inspector evaluates code from strings

Editors set BUN_INSPECT_CONNECT_TO for every process started from their terminals. Bun's VS Code extension does so by default. That variable does not mean that you asked to debug this program, so Bun prints a warning, does not connect, and runs the program:

warn: BUN_INSPECT_CONNECT_TO is ignored with --disallow-code-generation-from-strings=strict: the inspector evaluates code from strings

Every way to reach a debugger, and what =strict does with it:

RouteWith =strict
--inspect, --inspect-wait, --inspect-brk, BUN_INSPECTBun exits with an error at startup
BUN_INSPECT_CONNECT_TOBun prints a warning and connects to nothing
inspector.open() from node:inspectorthrows
startRemoteDebugger() from bun:jscthrows
new inspector.Session() from node:inspector, which needs no serverevaluates nothing, with or without the flag
$vm, JavaScriptCore's debugging global (BUN_JSC_useDollarVM=1)not defined

An in-process Session does not implement Runtime.evaluate, Runtime.compileScript, Runtime.runScript, Runtime.callFunctionOn or Debugger.evaluateOnCallFrame. Its other Debugger methods need inspector.open() first.

Release builds of Bun do not include $vm, so BUN_JSC_useDollarVM has no effect on them. A debug build of Bun has it, and leaves it out at both levels of the flag.

Parts of Bun that stop working#

One part of Bun calls new Function() itself: the development server. Every other row is an API that compiles a string you give it.

Part of BunFlag=strict
The development server: import() with import attributes of a module outside the bundlethrowsthrows
node:repl: repl.start(), REPLServerworksthrows
node:worker_threads: new Worker(code, { eval: true })worksthrows
node:inspector: inspector.open()worksthrows
bun:jsc: startRemoteDebugger()worksthrows
node:vm: everything that takes source textworksthrows
node:module: module._compile(), assigning Module.wrapperworksthrows
Bun.plugin(): onLoad and build.module() that return source textworksthrows

Every built-in module still loads with =strict. import { REPLServer } from "node:repl" is the one import that fails, because it reads REPLServer.

Tools that transpile through require.extensions, such as ts-node and @babel/register, do not work with =strict. Bun transpiles TypeScript and JSX itself, so a Bun program does not need them.

In a compiled executable#

Embed the flag when you compile. The executable then runs with it every time, with no arguments:

terminal
bun build --compile --compile-exec-argv="--disallow-code-generation-from-strings=strict" ./server.ts --outfile server
build.ts
await Bun.build({
  entrypoints: ["./server.ts"],
  compile: {
    execArgv: ["--disallow-code-generation-from-strings=strict"],
    outfile: "./server",
  },
});

Write the value with =. In --compile-exec-argv="--disallow-code-generation-from-strings strict", with a space, strict is a separate argument, which a compiled executable ignores. That executable runs with the flag and no value, and prints no error. The check in Checking that the flag is on catches the mistake.

BUN_OPTIONS can raise the level of a compiled executable. It cannot lower it.

A compiled executable does not read its own command line as Bun flags. ./server --preload ./x.js passes both arguments to the program in process.argv.

data: URLs written in the source#

The bundler resolves a data: URL that is written as a literal when it builds. That module is part of the executable, like any file the program imports, so it loads at both levels. A data: URL that the program puts together at run time is a string that becomes code, so =strict refuses it:

server.ts
await import("data:text/javascript,export default 1"); // bundled at build time: loads

const url = ["data:text/javascript", "export default 1"].join(",");
await import(url); // made at run time: EvalError

Without --compile there is no build step, so =strict refuses both.

Checking that the flag is on#

process.execArgv contains the flag:

server.ts
if (!process.execArgv.includes("--disallow-code-generation-from-strings=strict")) {
  throw new Error("refusing to start without --disallow-code-generation-from-strings=strict");
}

A Worker inherits process.execArgv, so the same check works inside one. When you give a Worker its own execArgv:

  • With =strict, Bun adds the flag to the list the Worker sees.
  • With the flag and no value, the Worker sees the list you gave it, as in Node.js. The Worker still refuses eval() and new Function().

What the flag does not cover#

  • Files. A program that writes a file and then imports it runs that file. Importing files is what a program does, so the flag allows it. Use file system permissions to limit what the process can write.
  • Startup. The entry point, a file given to --preload, --import or --require, and bun -e are the program. So is what you type into bun repl. The flag applies to what the program does once it runs. A data: URL given to --preload, --import or --require is not a file, and =strict refuses it like any other data: URL.
  • BUN_OPTIONS. Bun reads BUN_OPTIONS in a compiled executable too. It cannot lower the level, but --preload in BUN_OPTIONS loads a file before the entry point.
  • BUN_BE_BUN. With BUN_BE_BUN=1, a compiled executable runs as the bun CLI. The embedded program does not start, and the flags embedded with it do not apply.
  • Native code. A native addon, or a library loaded with bun:ffi, can do what native code can do. --no-addons refuses process.dlopen() and cc() from bun:ffi, and --no-ffi-cc refuses cc(). Neither one refuses dlopen(), linkSymbols() or CFunction() from bun:ffi.
  • Child processes. A process that the program spawns has its own flags.