Django · Server-side template injection (SSTI)

Server-side template injection (SSTI) in Django

Django templates loaded by name from disk are developer code; template injection starts when a view compiles a string that a user wrote, most often an email or notification template stored in the database.

Class
Server-side template injection (SSTI) in Python
CWE
CWE-1336, CWE-94
web framework
Django documentation
On this page

How it arises in Django

render(request, "page.html", context) looks a template up by name through the configured loaders. The user controls the context, never the template. Two APIs take template source instead:

  • django.template.Template(template_code), whose constructor takes the raw template code.
  • Engine.from_string(template_code), reached in projects as engines["django"].from_string(...).

Both are legitimate. They become an injection point in three recurring situations.

Customisable messages. A CRM lets each customer edit the body of outgoing emails with placeholders. The body is saved in a TextField and compiled with Template(body) when a message is sent. No request carries template syntax at send time; the source was planted days earlier by anyone who could edit that record.

Formatting inside the template string. Template("Hello " + name + ", your order {{ order.id }} shipped"). The developer wanted one variable in a hurry.

Preview endpoints. An editor posts the draft to a view that renders it so the author can see the result.

Django's template language is deliberately small. It cannot call functions with arguments or run Python expressions. What it can do is read attributes and dictionary keys of anything in the context and call methods that take no arguments. With a request or a user object in the context, that reaches settings-derived values, session data and related model rows. The Django documentation says it directly: the template system is not safe against untrusted template authors, who can perform XSS attacks and access properties of template variables that may contain sensitive information.

Vulnerable and fixed

from django.core.mail import send_mail
from django.template import Context, Template

from crm.models import Campaign


def send_campaign(campaign_id, recipient):
    campaign = Campaign.objects.get(pk=campaign_id)
    body = Template(campaign.body).render(Context({"user": recipient, "campaign": campaign}))
    send_mail(campaign.subject, body, "noreply@example.com", [recipient.email])

If customers only need placeholders, do not give them a template language. string.Template substitutes named fields and nothing else, and the mapping lists exactly which values are available:

from string import Template

from django.core.mail import send_mail

from crm.models import Campaign


def send_campaign(campaign_id, recipient):
    campaign = Campaign.objects.get(pk=campaign_id)
    fields = {"first_name": recipient.first_name, "company": recipient.company}
    body = Template(campaign.body).safe_substitute(fields)
    send_mail(campaign.subject, body, "noreply@example.com", [recipient.email])

If real template logic is a product requirement, render with an engine configured for it: no request in the context, plain dictionaries of strings instead of model instances, and no custom tags loaded.

What Django already does for you

The default path is safe: templates come from files, output is auto-escaped, and the language has no general expression evaluation. The documentation carries an explicit warning about untrusted template authors. The recommended way to create a Template is through the engine's factory methods (get_template(), select_template() and from_string()), which keeps template creation in a few findable places.

What es-python adds

The stored-template case is hard to see from the code or from the request, because the text that matters was saved long before the code that compiles it runs. es-python reports the operation: untrusted text reaching the Template constructor is recorded as ssti django, tagged ssti.

Which sources you switch on decides what "untrusted" covers. With the socket source, the preview endpoint and the string-concatenation view are reported, graded Critical. With the db_rows source on, values read from the database are followed as well, so the campaign body is reported at the moment it is compiled even though it arrived in an earlier request.

The event fires whether or not the text contained {{. The finding is that someone outside chose template source; today's value being harmless does not change that. When EyalSec can prove the text carried nothing the engine reads as syntax, the event is suppressed. The fixed version never constructs a Django template from the body, so there is no SSTI event at all. Block network code execution raises on ssti django.

Review checklist

  1. Search for Template( and from_string( and trace where each argument comes from.
  2. List every model field whose content is later compiled as a template.
  3. Offer placeholders through string.Template, not a template engine.
  4. Keep request, user and model instances out of contexts for user-authored templates.
  5. Restrict who can edit stored templates, and log changes.

References

See it on your own Django code

es-python runs your application unchanged and reports when request data reaches the operation, with where the data came from and the line that used it. A live demo walks through it on a real application.

EyalSec Pricing Docs Security Research Vulnerabilities Security glossary About Contact עברית Login Book a live demo