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:
| Flag | What it refuses |
|---|---|
--disallow-code-generation-from-strings | eval() and the Function constructors. The same as Node.js's flag. |
--disallow-code-generation-from-strings=strict | Every way a string becomes code in the process. Bun only. |
bun --disallow-code-generation-from-strings=strict server.tsnew Function("return 1 + 1"); // EvalError: Code generation from strings disallowed for this contextA 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 -eor 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
Workerhas the level of the process, whateverexecArgvyou give theWorker. - Every realm and context the program creates later has it:
ShadowRealm,node:vmcontexts under=strict, and everyBun.ModuleGraph. - The level a compiled executable is built with is a floor.
BUN_OPTIONScan 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
Workerstarts, as in Node.js.eval()andnew Function()throw inside it. - With
=strict, theWorkerconstructor throws theEvalError. Put the Worker's code in a file and pass the file's path or URL.
What each level refuses#
| A string becomes code through | Flag | =strict |
|---|---|---|
eval(), direct and indirect | refused | refused |
new Function(), and the async, generator and async generator function constructors, reached any way | refused | refused |
ShadowRealm.prototype.evaluate(), and eval() inside a ShadowRealm | refused | refused |
new vm.Script(), vm.runInThisContext(), vm.runInContext(), vm.runInNewContext() | allowed | refused |
vm.compileFunction(), new vm.SourceTextModule() | allowed | refused |
eval() inside a node:vm context, including one made with codeGeneration: { strings: true } | allowed | refused |
module._compile(), and a require.extensions handler that calls it | allowed | refused |
Assigning a different Module.wrapper | allowed | refused |
import, import() and require() of a data: or blob: URL that holds code | allowed | refused |
A Worker started from a data: or blob: URL, or with eval: true | allowed | refused |
A plugin that returns contents | allowed | refused |
| Macros | allowed | refused |
Bun.serve() with the experimental app option | allowed | refused |
inspector.open() from node:inspector, startRemoteDebugger() from bun:jsc | allowed | refused |
napi_run_script() in a native addon | allowed | refused |
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()andcreateRequire()of files and built-in modules.Bun.ModuleGraph.JSON.parse(),RegExp, andeval()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
exportsobject. import()of adata:orblob:URL that holds data, such as JSON, and of ablob:URL made fromBun.file(), which Bun reads from the file.vm.createContext()andnew 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 stringsEditors 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 stringsEvery way to reach a debugger, and what =strict does with it:
| Route | With =strict |
|---|---|
--inspect, --inspect-wait, --inspect-brk, BUN_INSPECT | Bun exits with an error at startup |
BUN_INSPECT_CONNECT_TO | Bun prints a warning and connects to nothing |
inspector.open() from node:inspector | throws |
startRemoteDebugger() from bun:jsc | throws |
new inspector.Session() from node:inspector, which needs no server | evaluates 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 Bun | Flag | =strict |
|---|---|---|
The development server: import() with import attributes of a module outside the bundle | throws | throws |
node:repl: repl.start(), REPLServer | works | throws |
node:worker_threads: new Worker(code, { eval: true }) | works | throws |
node:inspector: inspector.open() | works | throws |
bun:jsc: startRemoteDebugger() | works | throws |
node:vm: everything that takes source text | works | throws |
node:module: module._compile(), assigning Module.wrapper | works | throws |
Bun.plugin(): onLoad and build.module() that return contents | works | throws |
| Macros | work | fail |
Bun.serve() with the experimental app option | works | throws |
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:
bun build --compile --compile-exec-argv="--disallow-code-generation-from-strings=strict" ./server.ts --outfile serverawait 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 executable | works |
import(), require() or new Worker() of a JavaScript or TypeScript file on disk | refused, including a file that the program just wrote |
--preload, --import or --require in BUN_OPTIONS | refused: Bun prints the error and exits |
import() or require() of a .json, .jsonc, .json5, .toml, .yaml or .xml file on disk | works: 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:
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: EvalErrorWithout --compile there is no build step, so =strict refuses both.
Checking that the flag is on#
process.execArgv contains the flag:
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 theWorkersees. - With the flag and no value, the
Workersees the list you gave it, as in Node.js. TheWorkerstill refuseseval()andnew 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:
bun scripts/build.ts --profile=release --codeGenerationFromStrings=offThat 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=1and a missing--compile-exec-argvdo not change it. - The flag itself is still accepted, with or without
=strict, and changes nothing. process.execArgvlists only the flags you gave, as in any build. To check for this build, see thatnew vm.Script("")throws anEvalErrorwhen you gave no flag.- Bun's own code that compiles a string, such as
node:vm's andmodule._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,--importor--require, andbun -eare the program. So is what you type intobun repl. The flag applies to what the program does once it runs. Adata:URL given to--preload,--importor--requireis not a file, and=strictrefuses it like any otherdata:URL. BUN_OPTIONS. Bun readsBUN_OPTIONSin a compiled executable too. It cannot lower the level. With the flag and no value,--preloadinBUN_OPTIONSloads 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. WithBUN_BE_BUN=1, a compiled executable runs as thebunCLI. 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/memwithnode:fsdoes 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-addonsrefusesprocess.dlopen()andcc()frombun:ffi, and--no-ffi-ccrefusescc(). Neither one refusesdlopen(),linkSymbols()orCFunction()frombun:ffi. - Child processes. A process that the program spawns has its own flags.