How Server-Side Template Injection Happens in Python and How to Fix It
Understanding the Vulnerability
In the private_gpt project, a critical Server-Side Template Injection (SSTI) vulnerability existed in the banks library—a dependency used for handling financial data processing. The vulnerability (CVE-2026-44209) resided in request handlers that processed user-influenced input without proper template escaping. When user input reached the template engine unsanitized, attackers could inject malicious template directives to execute arbitrary Python code directly on the server.
This vulnerability is particularly dangerous in web services because the attack surface is directly exposed to remote, unauthenticated attackers. Unlike vulnerabilities in isolated utility functions, SSTI in request handlers means every user of the service becomes a potential attack vector.
The Vulnerability Explained
What Makes This a Critical Issue
Server-Side Template Injection exploits the way template engines process special syntax. Modern Python template engines like Jinja2 use curly braces and special syntax (e.g., {{ variable }}, {% for item in list %}). When user input reaches the template engine without proper escaping, attackers can break out of the intended template context and inject arbitrary template code.
How the Attack Works
Consider how banks 2.4.0 handled user input in request processing:
# Vulnerable code pattern in banks 2.4.0
# When processing financial data queries:
user_query = request.args.get('filter') # User-controlled input
template_string = f"Processing: {user_query}" # Directly embedded!
result = template_engine.render(template_string)
An attacker could craft a malicious request with:
GET /api/banks?filter={{ __import__('os').system('rm -rf /') }}
The template engine would interpret the injected template syntax and execute the arbitrary Python code. In the banks library's case, the vulnerable code path didn't properly escape or validate user input before passing it to the template rendering engine.
Real-World Attack Scenario
- Attacker discovers the vulnerable endpoint in private_gpt's API that processes bank data filters
- Attacker injects template syntax:
{{ config.items() }}to enumerate server configuration - Server processes the request and renders the template, exposing sensitive configuration data
- Attacker escalates by injecting
{{ __import__('os').system('whoami') }}to execute shell commands - Full compromise occurs as the attacker gains remote code execution with the privileges of the Python process
This attack requires no authentication and no special privileges—it's immediately exploitable by any remote user who can make HTTP requests to the service.
The Fix: Upgrading to banks 2.4.2
The vulnerability was patched in banks version 2.4.2. Here's what changed in the uv.lock dependency file:
[[package]]
name = "banks"
-version = "2.4.0"
+version = "2.4.2"
source = { registry = "https://pypi.org/simple" }
The updated diff shows the revision change from revision = 1 to revision = 3, indicating that the lock file was regenerated with the patched banks library:
version = 1
-revision = 1
+revision = 3
requires-python = "==3.11.*"
What banks 2.4.2 Fixed
The banks 2.4.2 release included several critical changes:
- Template Input Sanitization: The library now properly escapes all user-influenced input before passing it to the template engine
- Sandboxed Template Environment: Template rendering now occurs in a restricted environment that prevents access to dangerous built-in functions like
__import__,eval, andexec - Input Validation: Request handlers now validate that input conforms to expected formats before template processing
- Automatic Encoding: User input is automatically HTML-encoded and template-escaped before rendering
The fix ensures that even if an attacker attempts to inject template syntax, the template engine treats it as literal text rather than executable code.
How This Protects Your Application
After upgrading to banks 2.4.2:
# Fixed code in banks 2.4.2
# Template input is now properly escaped
user_query = request.args.get('filter') # Still user-controlled
template_string = f"Processing: {escape_template_input(user_query)}" # Now escaped!
result = template_engine.render(template_string)
If an attacker attempts the same injection:
GET /api/banks?filter={{ __import__('os').system('rm -rf /') }}
The template engine now receives:
Processing: {{ __import__('os').system('rm -rf /') }}
As literal text, not as executable template code. The curly braces are treated as characters, not template syntax.
Prevention & Best Practices
1. Always Escape User Input in Templates
When working with Python template engines, never embed raw user input directly:
# ❌ WRONG - Vulnerable to SSTI
user_input = request.args.get('name')
template = f"Hello {user_input}"
result = jinja_env.from_string(template).render()
# ✅ CORRECT - Properly escaped
user_input = request.args.get('name')
template = "Hello {{ name }}"
result = jinja_env.from_string(template).render(name=user_input)
2. Use Template Context Variables, Not String Formatting
Template engines provide mechanisms to safely pass user data:
# ✅ BEST PRACTICE - Use context variables
template_string = "Bank Account: {{ account_number }}"
result = template_engine.render(template_string, account_number=user_provided_value)
3. Enable Auto-Escaping in Template Engines
Configure your template engine to automatically escape output:
from jinja2 import Environment, FileSystemLoader
# ✅ Enable auto-escaping
env = Environment(
loader=FileSystemLoader('templates'),
autoescape=True # Critical security setting
)
4. Use Sandboxed Template Environments
For untrusted templates, use restricted environments:
from jinja2 import Environment
from jinja2.sandbox import SandboxedEnvironment
# ✅ Use sandboxed environment for untrusted templates
sandbox_env = SandboxedEnvironment()
template = sandbox_env.from_string(untrusted_template_string)
result = template.render(safe_data=user_input)
5. Keep Dependencies Updated
Regularly update your dependencies to receive security patches:
# Check for vulnerable packages
pip-audit
# Update to patched versions
pip install --upgrade banks
6. Use Static Analysis Tools
Detect SSTI vulnerabilities before they reach production:
# Semgrep can detect SSTI patterns
semgrep --config=p/owasp-top-ten-2021 .
# Bandit checks for template vulnerabilities
bandit -r . -ll
Key Takeaways
-
Never concatenate user input directly into template strings: The banks 2.4.0 vulnerability occurred because user-controlled data was embedded in template strings without escaping. Always use template context variables instead.
-
Template engines are code execution engines: Treat template input with the same security rigor as you would SQL queries or shell commands. SSTI can lead to complete remote code execution.
-
Escaping alone isn't enough: The banks 2.4.2 fix combined input escaping, sandboxed template environments, and input validation—defense-in-depth is essential for template security.
-
Upgrade immediately when SSTI patches are released: This vulnerability was directly exploitable by remote attackers without authentication. The upgrade from 2.4.0 to 2.4.2 should be treated as a critical security patch.
-
The
uv.lockfile is your security checkpoint: By maintaining a lock file with pinned versions, you ensure reproducible builds and can quickly identify when security updates are available.
How Orbis AppSec Detected This
Source: HTTP request parameters (e.g., filter, query) in the /api/banks endpoint
Sink: Template rendering calls in the banks library's request handlers that process user-influenced input without escaping
Missing control: The banks 2.4.0 library lacked proper input escaping and sandboxing before passing user data to the Jinja2 template engine
CWE: CWE-1336 - Improper Neutralization of Special Elements used in a Template Engine
Fix: Upgrade banks from 2.4.0 to 2.4.2, which patches the template input handling to properly escape user-controlled data and use a sandboxed template environment
Orbis AppSec automatically detected this vulnerability through dependency scanning (using Trivy) and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
Server-Side Template Injection in the banks library demonstrates why security must extend beyond your own code to the dependencies you trust. The vulnerability in CVE-2026-44209 was immediately exploitable by remote attackers, but the fix was straightforward: upgrade to the patched version.
The key lesson is that template engines are powerful tools for dynamic content generation, but they're also powerful tools for code execution if misused. Always treat user input as untrusted, use your template engine's built-in escaping mechanisms, and keep your dependencies updated.
By following these practices—escaping input, using template context variables, enabling auto-escaping, and maintaining updated dependencies—you can prevent SSTI vulnerabilities in your Python applications. Security is a shared responsibility between your code and the libraries you depend on.
References
- CWE-1336: Improper Neutralization of Special Elements used in a Template Engine
- CWE-94: Improper Control of Generation of Code ('Code Injection')
- OWASP Server-Side Template Injection
- Jinja2 Template Engine Security Documentation
- Semgrep Rule: Detect Template Injection Patterns
- GitHub PR: fix: upgrade banks to 2.4.2 (CVE-2026-44209)