The sandboxed environment

Jinja · Expert · 10 min

Letting users write templates (emails, themes, notifications) is a server-side template injection risk without a sandbox.

If template source comes from users, a normal Environment is dangerous: Jinja expressions can walk Python's object graph, for example {{ ''.__class__.__mro__[1].__subclasses__() }}, and reach functions that read files or run commands. This is server-side template injection (SSTI).

jinja2.sandbox.SandboxedEnvironment intercepts attribute access and calls:

  • underscore attributes such as __class__ and __globals__, and internal function and frame attributes, are blocked: they evaluate to a special undefined value that raises SecurityError as soon as it is used, and calling unsafe callables raises it too
  • range() is capped (MAX_RANGE, 100000 items) to stop trivially huge loops
  • ImmutableSandboxedEnvironment additionally blocks methods that mutate lists, dicts and sets, like append or update

You can customize policy by subclassing and overriding is_safe_attribute(obj, attr, value) and is_safe_callable(obj), or mark your own functions with @unsafe from the sandbox module.

What the sandbox does not do:

  • protect against resource exhaustion, such as nested loops or huge strings; enforce timeouts and output size limits yourself, ideally in a separate process
  • make unsafe objects safe: anything you pass into the context is reachable, so pass plain data (dicts, strings, numbers), never ORM objects with delete() methods or your app's config
  • escape output: autoescaping is still a separate setting

The safest design is still to not let users write templates at all. If you must, combine the sandbox with minimal context data, autoescaping and resource limits.

Example

from jinja2.sandbox import ImmutableSandboxedEnvironment, SandboxedEnvironment
from jinja2.exceptions import SecurityError
from jinja2 import Environment

evil = "{{ ''.__class__.__mro__[1].__subclasses__()|length > 0 }}"
print(Environment().from_string(evil).render())          # True: object graph reachable!

sandbox = SandboxedEnvironment(autoescape=True)
try:
    print(sandbox.from_string(evil).render())
except Exception as e:
    print(type(e).__name__)                              # SecurityError

user_template = "Hi {{ user.name }}, you have {{ count }} new messages"
print(sandbox.from_string(user_template).render(user={"name": "Ann"}, count=3))
# Hi Ann, you have 3 new messages

immutable = ImmutableSandboxedEnvironment()
try:
    immutable.from_string("{{ items.append(4) }}").render(items=[1, 2, 3])
except SecurityError as e:
    print("blocked:", e)        # blocked: access to attribute 'append' of 'list' object is unsafe.

Remember

  • SandboxedEnvironment blocks unsafe attributes and calls in untrusted templates
  • ImmutableSandboxedEnvironment also blocks mutating container methods
  • The sandbox doesn't limit CPU or memory, and can't protect unsafe objects you pass in

Watch out for

  • Rendering user-supplied template source with a normal Environment (SSTI).

Check yourself

  1. Which risk does SandboxedEnvironment NOT address on its own?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: jinja.palletsprojects.com/en/stable/sandbox/

The sandboxed environment · Jinja · Topolearn