---
title: "Keep input out of templates · Agent Wranglers"
url: https://agentwranglers.se/playbook/application-security/input-and-injection/template-injection
id: APS-5
level: Required
risk: takeover
updated: 2026-10-27
build: a2a6bdfed830
tokens: 876
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Keep input out of templates

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Input and injection](https://agentwranglers.se/playbook/application-security/input-and-injection.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-27.

- ID: APS-5
- Level: Required
- Chapter: [Input and injection](https://agentwranglers.se/playbook/application-security/input-and-injection.md)
- Rule: Pass user input to a template as data. Never use it as the template itself.
- Check: Put `{{7*7}}`, `${7*7}`, `#{7*7}` and `<%= 7*7 %>` in fields that end up in emails, PDFs or pages. The output shows the text as typed, never 49.
- Risk: An account or system taken over
- How: Load templates only from your own files. If customers write their own text, such as email messages, use a small placeholder list like `{name}` and replace only those. Run full template engines only in a sandbox. Prompts need the same split, as [keep user text out of the instructions](https://agentwranglers.se/playbook/ai-features/prompts-as-code/user-text-apart.md) says.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### Python example

```python
# Bad: the customer's text becomes the template
jinja_env.from_string(customer_text).render(order=order)
# Good: a fixed template, with the customer's text passed in as data
jinja_env.get_template("order-email.html").render(order=order, note=customer_text)
```

### JavaScript example

```js
// Bad: the customer's text becomes the template
ejs.render(customerText, { order });
// Good: only known placeholders are replaced, everything else stays text
const PLACEHOLDERS = { name: order.customerName, number: order.number };
const message = customerText.replace(/\{(\w+)\}/g, (match, key) => PLACEHOLDERS[key] ?? match);
```

## Sources

- [OWASP WSTG on testing for server-side Template Injection](https://wstg.owasp.org/latest/4-Web_Application_Security_Testing/07-Injection/18-Server-side_Template_Injection/)

## Related rules

- STK-84 [Use Playwright or WeasyPrint to turn HTML into PDF](https://agentwranglers.se/playbook/stack/product-services/html-to-pdf.md) in Software choices
- MED-22 [Render documents with no network and no local files](https://agentwranglers.se/playbook/files-and-media/generated-documents/sandboxed-rendering.md) in Files and media
- AIF-7 [Keep user text out of the instructions](https://agentwranglers.se/playbook/ai-features/prompts-as-code/user-text-apart.md) in AI features

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/application-security/input-and-injection/template-injection">Agent Wranglers application security playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Never pass input to a shell](https://agentwranglers.se/playbook/application-security/input-and-injection/command-injection.md)
- Next: [Strip line breaks from headers and logs](https://agentwranglers.se/playbook/application-security/input-and-injection/header-injection.md)
- [Input and injection](https://agentwranglers.se/playbook/application-security/input-and-injection.md)
