Why a Scripting Sandbox Is Only as Safe as Its Host-Access Policy
An embedded language engine is not a security boundary by default. What host access, reflection and process creation let guest code reach, and the deny-everything configuration that actually contains it.
Plenty of applications let users, or workflows, or plugins, supply a snippet of code to run: a formula, a filter expression, a transformation step. To keep that convenient without handing over the whole machine, developers reach for an embedded language engine like GraalVM, Nashorn, V8 isolates, or a Python interpreter, and call it a sandbox. The word does a lot of dishonest work. An in-process script engine is not a security boundary by default. It is a boundary only after you have explicitly stripped away everything the guest code could use to reach the host, and the default configuration of most of these engines does the opposite.
What a Language Sandbox Actually Is
When you embed a script engine, you create an execution context and hand it a string of code. The engine parses and runs that code in the same operating-system process as your application, sharing its memory, its file descriptors, its environment variables, and crucially its access to the host language's standard library. GraalVM, for instance, runs guest JavaScript or Python inside a polyglot context that can, if configured to, reach straight into Java objects and classes. That interop is the selling point. It is also the whole attack surface.
The engine's job is to run the code you gave it. It has no idea whether that code came from a trusted developer or an anonymous HTTP request. The isolation you get is whatever you configured, nothing more. If you never restricted host access, there is no sandbox, just a slightly awkward way to call Java or Python from a string.
How Host Access Leaks Out
The Orkes Conductor flaw disclosed as CVE-2026-58138 is the textbook version. Conductor built its GraalVM context with HostAccess.ALL for JavaScript and allowAllAccess(true) for Python. Those flags tell the engine to let guest code touch any host class it can name. From there the escape is short. A script uses reflection to look up java.lang.Runtime, gets the current runtime object, and calls exec() with a shell command. No memory corruption, no exotic primitive, just the guest asking the host politely for a class it should never have been allowed to see. Because the Conductor API had no authentication, that request could come from anyone, which is how a workflow definition turned into unauthenticated code execution. That incident is walked through in our news coverage of the Conductor RCE.
The same shape appears with any engine. In a Python sandbox, __builtins__, __import__, or a leftover reference to os reintroduces the whole standard library. In a JavaScript context with host bindings, one reachable Java or Node object is enough. Reflection is the universal skeleton key: even if you hide a class, if the engine allows reflection the guest can often find it by name. This is a cousin of how server-side template injection becomes remote code execution, where a templating language nobody thought of as a programming language turns out to be one.
What Locking It Down Requires
Conductor's fix is a good checklist because it enumerates the doors. The maintainers replaced blanket host access with an explicit denylist covering Class, ClassLoader, the reflection primitives (Method, Field, Constructor, Array), and the dangerous system classes (Runtime, ProcessBuilder, Process, System, Thread, ThreadGroup). Then they turned off the context's ability to load host classes, create threads or processes, make native calls, perform I/O, and read the environment, and they disabled script built-ins like load that pull in external code. Only after all of that is the engine doing what people assumed it did from the start.
The principle underneath the checklist is that a real sandbox is built by denial, not permission. Start from zero capability and grant back the specific, narrow things the legitimate use case needs, a math operation or a string transform, rather than starting from full access and trying to blocklist the bad parts. Blocklists lose, because the guest only needs one path you forgot and reflection helps it find that path.
Practical Guidance
If your application runs user-influenced expressions through an embedded engine, audit the context configuration first, not the input filtering. Look for HostAccess.ALL, allowAllAccess, unrestricted __builtins__, or any binding that exposes host objects, and confirm host class loading, reflection, process and thread creation, native access, filesystem and network I/O, and environment access are all off. Input sanitization is a distant second line; you cannot reliably regex your way out of a Turing-complete language with host access, and trying to invites the sentence-by-sentence escape game attackers always win.
Treat the sandbox as attacker-reachable code execution and threat-model accordingly. Run the engine as an unprivileged user in a container that itself has no more capability than the task needs, so that even a full escape lands somewhere contained rather than as root. Where you truly need to run untrusted code, prefer an out-of-process, OS-enforced boundary, a separate hardened worker, over an in-process language flag. And keep the engine and its host runtime patched, because the same operators who miss a quiet dependency fix are the ones who get surprised, a problem covered in tracking security fixes in open-source dependencies that never file an advisory.
A language sandbox can be made safe. It is just never safe for free, and the cost is paid in explicit, deliberate denial of every capability you did not mean to grant.