Running Untrusted Strategy Code Safely: Inside the Strategy Sandbox
Jonny Bravo
-Running Untrusted Strategy Code Safely: Inside the Strategy Sandbox
The most powerful feature of a quant platform is also its most dangerous: you let people write arbitrary Python and you run it on your servers. That's the whole value proposition — real code, not a drag-and-drop block builder that can't express a real strategy. But "run arbitrary Python on shared infrastructure" is, said plainly, remote code execution as a feature.
Get it wrong and a user's strategy reads your database credentials, exfiltrates another tenant's positions, or pivots into the rest of your monorepo. Most platforms solve this by not letting you write real code. We solve it with a layered sandbox, so you can.
This post is about that sandbox — what it blocks, how, and the honest framing of what it is and isn't.
Layer 1: reject the obvious at save time
The first line of defense runs the moment you hit save. Your code is parsed into an AST and walked by a validator that rejects anything that could escape the restricted environment — and it does so with precise line and column numbers, so the editor can render the error as a red squiggle exactly where the problem is.
The allowlist is short and deliberate. A strategy may import only numeric and authoring modules:
ALLOWED_IMPORTS = { "pandas", "numpy", "math", "statistics", "datetime", "typing", "dataclasses", "enum", "collections", "pydantic", "development.fund.backtest.base", "development.fund.backtest.indicators", }
No os, no subprocess, no requests, no socket. You can compute, you can't reach out.
Just as important is the list of names that must never be looked up — and the comments here show this was hardened against real escape techniques, not theoretical ones:
# getattr/setattr/delattr enable dynamic-dunder escapes that the static # visit_Attribute check can't see, e.g. getattr(self, "__" + "class__"). FORBIDDEN_NAMES = {"eval", "exec", "compile", "__import__", "open", "globals", "locals", "vars", "input", "breakpoint", "memoryview", "getattr", "setattr", "delattr"}
The classic Python sandbox escape is to climb the object graph: from any object, reach __class__, then __bases__, then __subclasses__(), and you can find your way to something that runs shell commands. The validator forbids those dunders outright:
FORBIDDEN_ATTRS = { "__class__", "__bases__", "__subclasses__", "__mro__", "__globals__", "__code__", "__builtins__", "__dict__", "__getattribute__", "__reduce__", ... }
And it closes the cleverer doors too: getattr(self, "__" + "class__") builds the attribute name from a string so a naive attribute-scanner never sees it — which is exactly why getattr itself is forbidden. "{0.__class__}".format(self) reaches a dunder through a format string, so format and format_map are forbidden methods. These aren't paranoia; each one is a known escape, named in the code, with the bypass spelled out in the comment.
Layer 2: restrict what's even available at runtime
AST validation is necessary but not sufficient — the same docstring says so explicitly: "This is NOT a security boundary on its own." Static analysis can always be outwitted by something clever enough. So the code is also executed in a deliberately impoverished environment.
When the strategy loads, it runs with a hand-picked builtins subset — the names numeric code actually needs, and nothing more:
_SAFE_BUILTIN_NAMES = { "abs", "min", "max", "sum", "len", "range", "enumerate", "zip", "map", "filter", "sorted", "round", "float", "int", "bool", "str", "list", "dict", "tuple", "set", "any", "all", "isinstance", "hasattr", "print", ... # required machinery for class / f-strings / comprehensions under restricted builtins "__build_class__", "__name__", }
If __import__ isn't in scope and open isn't in scope, a strategy can't import a forbidden module or touch the filesystem even if it found a way past the AST check. The two layers cover each other's blind spots.
Layer 3: an importer that won't share the monorepo
Even the import that is allowed gets policed. The restricted importer permits only an exact allowed module or a true submodule of one — and the comment records a real bug this guards against:
def _restricted_import(name, globals=None, locals=None, fromlist=(), level=0): # allow ONLY an exact allowed module or a true SUBMODULE of one. The old # `a == root or a.startswith(root + ".")` matched `import development` against the # allowed `development.fund.backtest.base` (shared root), letting strategy code reach # the whole monorepo (DB engines, _secrets, ...). allowed = any(name == a or name.startswith(a + ".") for a in ALLOWED_IMPORTS) if not allowed: raise ImportError(f"import of '{name}' is not allowed in strategy code") return _builtins.__import__(name, globals, locals, fromlist, level)
import development.fund.backtest.base is fine. import development — which would have shared a root prefix and opened a door to the database engines and the secrets module — is rejected. One subtle prefix bug is the difference between a sandbox and a sieve, and it's closed.
Honest about the boundary
What we don't do is pretend a Python sandbox is a hard isolation guarantee. The code says it plainly: AST validation plus restricted builtins plus a restricted importer is a strong defense-in-depth posture, but it "is NOT a security boundary on its own — it is paired with restricted exec globals and Airflow execution timeouts. v2 hardening = subprocess + prlimit."
That's the right way to think about it. The sandbox raises the cost of an escape enormously and catches every known technique, and it sits inside an execution environment with timeouts and resource limits, with subprocess-level isolation as the hardening path. Layered, honest, and improving — not a single magic wall claimed to be unbreakable.
Why this is what lets you offer real code
A lot of "no-code strategy builders" exist precisely because letting users write real Python safely is hard. They trade expressiveness for safety and end up with a toy.
By investing in a genuine layered sandbox, we get to offer the opposite trade: write actual Python — pandas, numpy, your own indicator math, real control flow — and run it on shared infrastructure, with the platform absorbing the security burden. The quant gets a real language. The platform stays safe. And the next tenant's positions stay theirs.
That's not a feature you see. It's the reason the features you do see are allowed to exist.
Write real Python, sandboxed by default — the editor flags an escape attempt before you can even run it. Open the strategy editor →