The Jinja2 Sandbox: How MockBase Templates Can't Read Your Filesystem
Dynamic mock responses need a templating language. Jinja2 is the obvious choice — but a full Jinja2 environment lets a template call arbitrary Python: read files, import modules, walk object internals via __class__.__mro__. Handing that to anyone who can type a response body is not an option on a public service, so MockBase renders every template through Jinja2's SandboxedEnvironment instead of the default one.
What sandboxing actually blocks
jinja2.sandbox.SandboxedEnvironment restricts attribute and item access at render time: template code can't reach "unsafe" attributes (dunder methods, memory internals) or call the small set of things Jinja2 flags as dangerous. It's a real, load-bearing security boundary — not a lint rule — and it's the same class Jinja2 itself recommends for exactly this situation: rendering templates you don't fully trust the author of.
The context passed into every template is deliberately narrow. A template only ever sees:
request.method,request.pathrequest.query.<name>,request.headers.<name>,request.body.<path>,request.raw_bodyrequest.path_params.<name>- three helper functions:
now_iso(),uuid4(),random_int(low, high)
That's the entire surface. No filesystem, no network, no imports, no access to anything outside the current request.
Random numbers, done properly
random_int() doesn't call Python's random module. It uses secrets.randbelow() — a CSPRNG — because a mock response is exactly the kind of thing someone might script against for testing token generation, sampling, or retry logic, and a predictable PRNG seed is a bad thing to bake into a tool people use for security-adjacent testing. It's a small detail, but it's the kind of detail that's easy to get wrong by defaulting to random.randint.
The limits nobody sees until they hit them
Sandboxing stops a template from escaping to do something dangerous. It doesn't stop a template from being merely expensive — a deeply nested {% for %} loop or a huge input string can still burn CPU and memory even inside a sandbox. MockBase enforces three separate ceilings on every render:
| Limit | Default | What it catches |
|---|---|---|
| Input template size | 20,000 characters | An oversized template string |
| Rendered output size | 100,000 characters (cumulative across the whole response) | A template that expands into something huge |
| Structure depth | 20 levels | Runaway recursion in nested JSON |
Any violation raises immediately and the request fails closed — MockBase returns an error rather than a partially-rendered or truncated response. The output-size budget is cumulative across the entire response body, not per-field, so ten fields each rendering 15,000 characters of output still trips the limit even though no single field looks abusive on its own.
Why this matters for a mock API specifically
A "mock API" sounds like a toy, but the templates run server-side, on shared infrastructure, on every request to the mock — including anonymous ones nobody has reviewed. Sandboxing plus hard resource limits is what makes it safe to let arbitrary users write arbitrary Jinja2 and have it execute on your servers thousands of times a day.
Xerat02
Building MockBase.
More from the blog
Your Mock's Access Token Is Shown to You Exactly Once
Turn on token protection and MockBase generates a token, shows it to you, and then — by design — never shows it to you again. Not in the UI, not through the API. If you didn't copy it, your only move is to rotate: generate a new one and invalidate the old.
Custom Subdomains Match Exactly One Label, on Purpose
Claim a slug and your mock also answers at `<slug>.mockbase.org` instead of only `mockbase.org/mock/<id>`. The routing behind that is a single Host-header check, and it's stricter than it might look — deliberately.