# Server-Side Template Injection (SSTI)

Language: Python
Severity: Critical
CWE: CWE-94

## Source
9

## Flow
9-11

## Sink
11

## Vulnerable Code
```python
from jinja2 import Environment
from flask import Flask, request, jsonify
app = Flask(__name__)
jinja_env = Environment()
@app.route('/api/v1/report/preview', methods=['POST'])
def render_report_preview():
    payload = request.json
    rpt_header = payload.get('header_template', '')
    rpt_body = payload.get('body_template', '')
    combined_tpl = f"{rpt_header}\n{rpt_body}"
    compiled = jinja_env.from_string(combined_tpl)
    ctx_data = {'org': payload.get('org_name', 'N/A'), 'period': payload.get('fiscal_period', 'Q1')}
    rendered_output = compiled.render(**ctx_data)
    return jsonify({'preview': rendered_output})
```

## Explanation

User-controlled JSON fields 'header_template' and 'body_template' are extracted directly from the request payload at lines 8-9, concatenated into a single template string at line 10, and then passed to jinja_env.from_string() at line 11 without any sanitization, validation, or sandboxing. This allows an attacker to inject arbitrary Jinja2 template expressions that get executed server-side, potentially leading to remote code execution.

## Remediation

The fix replaces the standard Jinja2 `Environment` with `SandboxedEnvironment`, which restricts access to unsafe attributes and methods during template rendering, preventing code execution even if a malicious expression slips through. Additionally, a strict input validation layer (`validate_template`) is applied to both user-supplied template fields before rendering, rejecting any templates containing dunder access (`__`), logic blocks (`{% %}`), bracket notation, or complex expressions beyond simple variable placeholders. This defense-in-depth approach ensures that even if one layer is bypassed, the other still prevents exploitation.

## Secure Code
```python
from jinja2 import SandboxedEnvironment
from flask import Flask, request, jsonify
import re

app = Flask(__name__)
jinja_env = SandboxedEnvironment()

# Allowlist: only permit simple variable placeholders and basic filters
SAFE_TEMPLATE_PATTERN = re.compile(
    r'^[^{]*(?:\{\{\s*[a-zA-Z_][a-zA-Z0-9_.]*(?:\s*\|\s*[a-zA-Z_]+)?\s*\}\}[^{]*)*$'
)

def validate_template(template_str, field_name):
    """Validate that a template string only contains safe placeholders."""
    if not isinstance(template_str, str):
        raise ValueError(f"'{field_name}' must be a string.")
    if len(template_str) > 5000:
        raise ValueError(f"'{field_name}' exceeds maximum allowed length.")
    # Block dangerous patterns: attribute/item access chains, underscores (dunder access),
    # and executable constructs like {% %} blocks
    if '{%' in template_str or '%}' in template_str:
        raise ValueError(f"'{field_name}' contains disallowed template logic blocks.")
    if '__' in template_str:
        raise ValueError(f"'{field_name}' contains disallowed dunder access.")
    if not SAFE_TEMPLATE_PATTERN.match(template_str):
        raise ValueError(f"'{field_name}' contains disallowed template expressions.")
    # Block bracket notation access (e.g., config['key'] or obj["key"])
    if re.search(r'\[', template_str):
        raise ValueError(f"'{field_name}' contains disallowed bracket access.")


@app.route('/api/v1/report/preview', methods=['POST'])
def render_report_preview():
    payload = request.json
    if not payload or not isinstance(payload, dict):
        return jsonify({'error': 'Invalid JSON payload.'}), 400

    rpt_header = payload.get('header_template', '')
    rpt_body = payload.get('body_template', '')

    try:
        validate_template(rpt_header, 'header_template')
        validate_template(rpt_body, 'body_template')
    except ValueError as e:
        return jsonify({'error': str(e)}), 400

    combined_tpl = f"{rpt_header}\n{rpt_body}"

    try:
        compiled = jinja_env.from_string(combined_tpl)
        ctx_data = {
            'org': payload.get('org_name', 'N/A'),
            'period': payload.get('fiscal_period', 'Q1')
        }
        rendered_output = compiled.render(**ctx_data)
    except Exception:
        return jsonify({'error': 'Template rendering failed.'}), 400

    return jsonify({'preview': rendered_output})
```
