SandboxedEnvironment is a drop-in replacement for Environmentthat adds
defense-in-depth controls for attribute access, function calls, imports, and
resource consumption at render time.
Security boundary: The sandbox is not process isolation. Do not rely on it alone for adversarial template source. Curate the render context and add process/container controls for CPU, memory, filesystem, network, and time.
from kida import SandboxedEnvironment
All templates compiled by this environment are subject to sandbox restrictions. No code changes are needed in your templates -- the sandbox operates transparently at the engine level.
Quick Start
from kida import SandboxedEnvironment
env = SandboxedEnvironment()
# Safe: normal attribute access works
tmpl = env.from_string("Hello, {{ user.name }}!")
tmpl.render(user={"name": "Alice"}) # "Hello, Alice!"
# Blocked: dunder access raises SecurityError
tmpl = env.from_string("{{ user.__class__.__mro__ }}")
tmpl.render(user="hello") # raises SecurityError
The default policy blocks all dunder attributes (except a safe subset like
__len__ and __iter__), prevents access to and direct calls of untrusted
Python function, type, and code objects, disables imports, and limitsrange()
to 10,000 elements. Functions registered as environment globals and functions
compiled from Kida{% def %}/ component templates are trusted call targets.
Security Policy Configuration
Customize the sandbox by passing aSandboxPolicyto the environment:
from kida.sandbox import SandboxPolicy, SandboxedEnvironment
policy = SandboxPolicy(
allowed_attributes={"name", "title", "email", "items"},
blocked_types={type, type(lambda: 0)},
max_output_size=50_000,
max_range=1000,
)
env = SandboxedEnvironment(sandbox_policy=policy)
All Options
| Option | Type | Default | Description |
|---|---|---|---|
allowed_attributes |
frozenset[str] | None |
None |
If set, only these attribute names are accessible (plus safe collection methods).Nonemeans all non-blocked attributes are allowed. |
blocked_attributes |
frozenset[str] |
frozenset() |
Additional attribute names to block, merged with the built-in blocked set. |
blocked_types |
frozenset[type] |
frozenset() |
Object types that cannot be accessed at all. Function, type, and code objects are always blocked regardless of this setting. |
allow_import |
bool |
False |
Whether__import__is available in templates. |
allow_mutating_methods |
bool |
False |
Whether mutating collection methods (append, pop, clear, etc.) are accessible. |
allow_calling |
frozenset[str] | None |
None |
Set of type names whose instances may be called.None permits non-blocked callable types; untrusted Python functions remain blocked. An empty frozenset()blocks all calls, including Kida component and environment-global calls. |
max_output_size |
int | None |
None |
Maximum cumulative render output length in characters across full, block, sync-stream, and async-stream surfaces. RaisesSecurityError (K-SEC-005) when exceeded. Nonemeans unlimited. |
max_range |
int |
10000 |
Maximumrange()size allowed in templates. |
SandboxPolicyis a frozen dataclass -- construct a new instance for each configuration.
Method Restrictions
The sandbox distinguishes between read-only and mutating methods on built-in collection and string types.
Read-Only Methods (Always Allowed)
These methods are allowed by default because they do not modify application state:
| Type | Methods |
|---|---|
| dict | items, keys, values, get, copy |
| sequence | count, index |
| set | union, intersection, difference, issubset, issuperset, symmetric_difference |
| string | startswith, endswith, strip, lstrip, rstrip, split, rsplit, join, replace, lower, upper, title, capitalize, format, encode, decode, find, rfind, removeprefix, removesuffix |
Mutating Methods (Blocked by Default)
These methods are blocked unless the policy explicitly opts in:
append, extend, insert, pop, remove, sort, reverse, clear, update, add, discard
See Allowlists for how to enable them.
Dunder Access
Most dunder attributes are blocked. The following safe subset is permitted:
__len__-- needed forlengthfilter and truthiness checks__iter__-- needed for{% for %}loops__contains__-- needed forinoperator__getitem__-- needed for bracket access (items[0])__str__,__repr__-- needed for string output
All other dunder attributes (__class__, __subclasses__, __bases__, __mro__, __dict__, __globals__, etc.) are unconditionally blocked.
Call-Time Safety Checking
The sandbox intercepts every attribute access and function call at render time -- not at compile time. This means:
- Attribute access is checked against the policy before the value is returned. Blocked attributes raise
SecurityErrorimmediately. - Type checks run on the object being accessed. If the object's type is in the unsafe set (or
blocked_types), access is denied regardless of the attribute name. - Function calls are always intercepted. Fundamentally unsafe callable
types are denied unless the callable is an application-registered
environment global or a Kida-compiled function. When
allow_callingis configured, the callable's type name must also be in that allowlist.
from kida import SandboxedEnvironment, SecurityError
env = SandboxedEnvironment()
try:
tmpl = env.from_string("{{ func.__globals__ }}")
tmpl.render(func=lambda: None)
except SecurityError as e:
print(e) # "Access to attribute '__globals__' is blocked by sandbox policy"
The sandbox also replaces range()with a size-limited version:
env = SandboxedEnvironment()
tmpl = env.from_string("{% for i in range(999999) %}x{% endfor %}")
tmpl.render() # raises SecurityError: range() size 999999 exceeds sandbox limit of 10000
Allowlists
allow_mutating_methods
Enable mutating methods when templates need to build data structures:
from kida.sandbox import SandboxPolicy, SandboxedEnvironment
policy = SandboxPolicy(allow_mutating_methods=True)
env = SandboxedEnvironment(sandbox_policy=policy)
tmpl = env.from_string("""
{% set items = [] %}
{% for name in names %}
{% do items.append(name.upper()) %}
{% endfor %}
{{ items | join(", ") }}
""")
tmpl.render(names=["alice", "bob"])
Warning: Enabling mutating methods allows templates to modify objects passed in via the context. Only enable this when templates are semi-trusted or context objects are copies.
allow_calling
Restrict which types of callables templates can invoke:
from kida.sandbox import SandboxPolicy, SandboxedEnvironment
# Only allow calling methods on built-in types
policy = SandboxPolicy(
allow_calling=frozenset({"builtin_function_or_method", "method"}),
)
env = SandboxedEnvironment(sandbox_policy=policy)
When allow_calling is None(the default), non-blocked callable types are
permitted after attribute checks. Arbitrary Python functions supplied only in
render context remain blocked; register trusted application functions with
env.add_global(...). Set an empty frozenset()to block every function call,
including Kida components and registered globals:
# Block all calls -- templates can only read attributes
policy = SandboxPolicy(allow_calling=frozenset())
allowed_attributes
Lock down attribute access to an explicit allowlist:
policy = SandboxPolicy(
allowed_attributes=frozenset({"name", "email", "title", "created_at"}),
)
env = SandboxedEnvironment(sandbox_policy=policy)
# Only .name, .email, .title, .created_at are accessible
# (plus safe collection methods like .items, .keys, .get)
When allowed_attributesis set, any attribute not in the allowlist and not in the safe collection methods set is blocked.
Examples
CMS with User-Submitted Templates
from kida.sandbox import SandboxPolicy, SandboxedEnvironment
policy = SandboxPolicy(
allowed_attributes=frozenset({
"title", "body", "author", "published_at",
"name", "email", "avatar_url",
"items", "keys", "values",
}),
max_output_size=100_000, # 100KB limit
max_range=100,
)
env = SandboxedEnvironment(sandbox_policy=policy)
user_template = db.get_template(page_id)
tmpl = env.from_string(user_template)
html = tmpl.render(page=page, site=site_config)
Customer-Editable Email Templates
from kida.sandbox import SandboxPolicy, SandboxedEnvironment
policy = SandboxPolicy(
allowed_attributes=frozenset({
"first_name", "last_name", "email", "company",
"order_id", "total", "items",
"name", "quantity", "price",
}),
allow_calling=frozenset(), # No function calls
max_output_size=50_000,
max_range=50,
)
env = SandboxedEnvironment(sandbox_policy=policy)
template_source = customer.get_email_template("welcome")
tmpl = env.from_string(template_source)
html = tmpl.render(user=user, order=order)
Plugin System with Semi-Trusted Code
from kida.sandbox import SandboxPolicy, SandboxedEnvironment
policy = SandboxPolicy(
allow_mutating_methods=True, # Plugins may build data
allow_import=False, # No imports
blocked_attributes=frozenset({"password", "secret_key", "api_key"}),
blocked_types=frozenset({type, type(lambda: 0)}),
max_range=5000,
)
env = SandboxedEnvironment(sandbox_policy=policy)
Limitations
- Compile-time only via runtime checks. The sandbox does not statically analyze templates. It intercepts attribute access and calls at render time, so a blocked operation only raises
SecurityErrorwhen the code path executes. - No CPU time limits. The sandbox limits
range()size and output length, but cannot prevent infinite loops or expensive computations. Use OS-level timeouts (e.g.,signal.alarmor process-level limits) for CPU protection. - No filesystem or network restrictions. The sandbox controls template-level access. If a context object exposes methods that perform I/O, the sandbox does not intercept the I/O itself -- only access to the method. Audit what you pass into the template context.
- Mutating methods affect the caller. When
allow_mutating_methods=True, templates can modify mutable objects passed in the context. Pass copies if you need to preserve originals. - Type-name matching for allow_calling. The
allow_callingoption matches ontype(obj).__name__, not the actual type object. Two unrelated types with the same__name__would both be permitted.
See Also
- Security Hardening -- Context-specific escaping, URL validation, and resource limits
- Configuration -- Environment options reference