ISSUES CLOSED: #319
4.3 KiB
Template Security
Overview
The cleveragents.templates.secure_renderer module provides a sandboxed
template renderer that prevents template injection attacks while
supporting simple variable substitution.
Threat Model
Template injection occurs when user-controlled input is embedded in
template strings and evaluated. Python's str.format() is particularly
dangerous because it allows:
- Attribute access:
{obj.__class__.__init__.__globals__}can leak sensitive module-level data. - Index access:
{items[0]}can read arbitrary collection entries. - Format specs:
{val:>10}can cause unexpected formatting or DoS. - Conversion flags:
{val!r}can change output representation.
The secure renderer rejects all of these, only allowing {name} style
substitution with simple identifiers.
Allowed Syntax
Only the following template syntax is permitted:
{variable_name}
Where variable_name matches \w+ (letters, digits, underscore).
Everything else is rejected:
| Pattern | Risk | Result |
|---|---|---|
{x.y} |
Attribute access | Rejected |
{x[0]} |
Index access | Rejected |
{x:>10} |
Format spec | Rejected |
{x!r} |
Conversion flag | Rejected |
{fn()} |
Function call | Rejected |
{{ x }} |
Jinja2 expression | Rejected |
{% block %} |
Jinja2 block | Rejected |
Configuration
Use TemplateConfig to configure the renderer:
from cleveragents.templates.secure_renderer import (
SecureTemplateRenderer,
TemplateConfig,
)
config = TemplateConfig(
allowed_keys=frozenset({"name", "description", "role"}),
max_template_length=10_000, # default
max_output_length=50_000, # default
reject_unknown_keys=True, # default
log_rejected=True, # default
)
renderer = SecureTemplateRenderer(config=config)
Parameters
allowed_keys: Set of permitted placeholder names. Empty means any simple identifier is allowed (open mode).max_template_length: Maximum raw template string length (default: 10,000). Prevents DoS via oversized templates.max_output_length: Maximum rendered output length (default: 50,000). Prevents DoS via value expansion.reject_unknown_keys: Whether to raise on keys not in the allowlist (default:True). Ignored whenallowed_keysis empty.log_rejected: Log rejected keys at WARNING level (default:True).
Pre-validation
Use validate_template() at action/plan creation time to catch
issues early:
from cleveragents.templates.secure_renderer import validate_template
errors = validate_template(
template_text,
allowed_keys=frozenset({"name", "description"}),
)
if errors:
raise ValueError(f"Invalid template: {errors}")
Convenience Function
For one-off renders without creating a renderer instance:
from cleveragents.templates.secure_renderer import render_template_secure
result = render_template_secure(
"Hello {name}",
{"name": "World"},
allowed_keys=frozenset({"name"}),
)
Safe Substitution Behaviour
If a placeholder key is not found in the context, it is left as-is in the output (safe-substitute behaviour):
renderer.render("Hello {name}, {role}", {"name": "Alice"})
# => "Hello Alice, {role}"
Legacy Compatibility
The existing TemplateRenderer in cleveragents.templates.renderer
now wraps SecureTemplateRenderer internally. It returns the raw
template on any rendering failure to preserve backward compatibility,
and logs a WARNING when doing so (enable DEBUG for the full template).
An optional config parameter allows callers to pass a
TemplateConfig through to the underlying secure renderer:
from cleveragents.templates.renderer import TemplateRenderer
from cleveragents.templates.secure_renderer import TemplateConfig
renderer = TemplateRenderer(
config=TemplateConfig(allowed_keys=frozenset({"name"})),
)
Exception Hierarchy
TemplateError (base)
├── TemplateSecurityError — unsafe constructs
├── TemplateSizeError — length limits exceeded
└── TemplateValidationError — allowlist violations