# 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 ```