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.

How =strict decides#

With =strict, the engine runs static code and nothing else. Static code is:

  • Bun's own built-in modules and functions.
  • The modules that Bun's loader reads from where the program is stored: files on disk or, for a compiled executable, the executable itself.
  • The program given to bun -e or on standard input.

Everything else counts as made at run time, and the engine refuses it at the one place where code is made ready to run. The check does not depend on the API that the code came through, so it also holds for a way in that the tables below do not list.

Some APIs refuse earlier, so that the error is where the string is: new vm.Script() throws, though nothing has run yet.

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: URL that holds codeallowedrefused
A Worker started from a data: or blob: URL, or with eval: trueallowedrefused
A plugin that returns contentsallowedrefused
Macrosallowedrefused
Bun.serve() with the experimental app optionallowedrefused
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.
  • import() of a data: or blob: URL that holds data, such as JSON, and of a blob: URL made from Bun.file(), which Bun reads from the file.
  • vm.createContext() and new vm.SyntheticModule(). Neither one compiles a string.

A compiled executable with =strict loads modules, Workers and require()d files from the executable only. See Files on disk are not part of the program.

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 contentsworksthrows
Macrosworkfail
Bun.serve() with the experimental app optionworksthrows

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.

Files on disk are not part of the program#

A compiled executable is stored in itself. With =strict, what it loads from disk is not static code, so the engine refuses it.

At run time=strict in a compiled executable
import(), require() or new Worker() of a file in the executableworks
import(), require() or new Worker() of a JavaScript or TypeScript file on diskrefused, including a file that the program just wrote
--preload, --import or --require in BUN_OPTIONSrefused: Bun prints the error and exits
import() or require() of a .json, .jsonc, .json5, .toml, .yaml or .xml file on diskworks: Bun makes the value without making code
import() of a database on disk with { type: "sqlite" }works
import() or require() of a .txt or .md file on disk, or of any file with { type: "text" }refused: read the file with Bun.file() instead

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().

A build of Bun that always has =strict#

When you build Bun from source, turning the codeGenerationFromStrings build option off makes =strict a compile-time constant:

terminal
bun scripts/build.ts --profile=release --codeGenerationFromStrings=off

That binary refuses what --disallow-code-generation-from-strings=strict refuses, with no flag:

  • It reads no flag or environment variable to get the level, so BUN_BE_BUN=1 and a missing --compile-exec-argv do not change it.
  • The flag itself is still accepted, with or without =strict, and changes nothing.
  • process.execArgv lists only the flags you gave, as in any build. To check for this build, see that new vm.Script("") throws an EvalError when you gave no flag.
  • Bun's own code that compiles a string, such as node:vm's and module._compile()'s, sits after a refusal that always happens, so an optimized build can leave it out.

JavaScriptCore's own eval() and Function code is still in the binary. It is switched off for every global object, and Bun has no code left that switches it on.

The option is on in the builds of Bun that we publish.

What the flag does not cover#

  • Files. Outside a compiled executable, 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, or compile the program.
  • 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. With the flag and no value, --preload in BUN_OPTIONS loads a file before the entry point.
  • The executable file. Whoever can replace or edit a compiled executable decides what is embedded in it.
  • 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.
  • The process's own memory. A program that can write to its own memory can change anything, including the embedded modules and Bun itself. On Linux, writing to /proc/self/mem with node:fs does that. Native code can do it everywhere.
  • 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.