146 lines
4.3 KiB
Markdown
146 lines
4.3 KiB
Markdown
# 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:
|
|
|
|
```python
|
|
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 when `allowed_keys` is 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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):
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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
|
|
```
|