Server-Side Template Injection via User-Supplied Objects

Table of Contents
| Platform | PortSwigger Web Security Academy |
| Topic | Server-side template injection |
| Lab | SSTI with information disclosure via user-supplied objects |
| Difficulty | Practitioner |
| Engine | Django (Python) |
| Goal | Leak the framework’s SECRET_KEY |
| Tools | Browser, the engine’s own documentation |
The idea#
Not every SSTI ends in remote code execution. Django’s template language is
deliberately sandboxed, there’s no {{7*7}} arithmetic, no imports, no
os.system. But a sandbox that blocks code doesn’t necessarily block data:
if a sensitive object is reachable from the template context, you can still read
it. This lab leaks Django’s SECRET_KEY by doing exactly that.
Reaching the template#
Getting to the product template editor is the same starting point as the
documentation lab
:
log in as content-manager, open a product, and click Edit template. Rather
than repeat those steps, I’ll pick up from the editor.

Fingerprinting the engine#
The template interpolates objects with {{ ... }}, e.g. {{product.stock}} and
{{product.name}}. Several engines use that syntax, so provoke an error with a
burst of template metacharacters that are each special in a different engine (so
one is sure to be invalid for whatever parser is running),
${{<%[%'"}}%\, and preview:

The stack trace names the engine outright:

Reading the documentation#
Since Django’s templates are sandboxed against code execution, we pivot from RCE to information disclosure, and the engine’s own docs point the way. Search for the Django template documentation:

Django ships a built-in {% debug %} tag that outputs a load of debugging
information, including the current template context and the imported modules:

One catch worth knowing: Django’s error-page debug deliberately excludes
sensitive settings, anything whose name contains KEY, SECRET, PASS,
SIGNATURE, or TOKEN, so SECRET_KEY will never show up in a raw stack trace:

Dumping the context#
That filtering only applies to the error page. The {% debug %} tag rendered
inside the template is not filtered the same way, so inject it:
{% debug %}
The rendered output dumps the whole template context, and alongside product
there’s a settings object in scope:


Leaking the secret key#
settings is reachable, so just read the attribute directly, no code execution
needed:
{{settings.SECRET_KEY}}Render it and Django prints the signing key straight into the page. Submit that value to solve the lab.

Real-world impact#
This lab leaks a value rather than running code, but a disclosed Django
SECRET_KEY is serious. The key signs and verifies everything Django trusts.
With it, in a real, authorized engagement, an attacker can:
- Forge session cookies — mint a valid signed session for any user, including staff/admin, for a full account takeover with no password.
- Forge other signed data — password-reset tokens, signed URLs, cookies, and
any
signing/dumps()payloads the app relies on. - Pivot to deeper compromise — admin or privileged access often exposes further functionality (file uploads, template editing, integrations) that can be chained toward code execution on the server.
So an “information disclosure” finding here is really the key to impersonation
and, frequently, full application compromise, which is why a leaked SECRET_KEY
must be rotated immediately. Only ever against systems you are authorized to test.
Remediation#
- Don’t expose sensitive objects to templates. Pass only the specific,
non-sensitive values a template needs, never whole config/
settingsobjects. - Keep user input out of template source. Render fixed templates and bind user data as context values.
- Rotate a leaked
SECRET_KEYimmediately, it signs sessions, password-reset tokens, and more; disclosure can lead to session forgery.
A concrete, generic example, putting only the needed values in the context rather than whole objects:
# Unsafe: the whole settings object is reachable from the template
render(request, "product.html", {"settings": settings, "product": p})
# Safe: expose only the specific, non-sensitive values the template needs
render(request, "product.html", {"product_name": p.name, "stock": p.stock})Key takeaways#
- SSTI is not only RCE, in a sandboxed engine it can still be a powerful information-disclosure primitive.
{% debug %}is the fastest way to see what’s actually reachable in a Django template’s context.- A reachable
settingsobject turns one line,{{settings.SECRET_KEY}}, into a full secret-key leak.
Related posts#
- SSTI in an Unknown Language — the previous lab, fingerprinting a hidden engine.
- SSTI in a Sandboxed Environment — escaping a sandbox with Java reflection.
- Full series: PortSwigger: Server-Side Template Injection .