{"title":"Server-Side Template Injection (SSTI)","language":"Python","severity":"Critical","cwe":"CWE-94","source_lines":[9],"flow_lines":[9,11],"sink_lines":[11],"vulnerable_code":"from jinja2 import Environment\nfrom flask import Flask, request, jsonify\napp = Flask(__name__)\njinja_env = Environment()\n@app.route('/api/v1/report/preview', methods=['POST'])\ndef render_report_preview():\n    payload = request.json\n    rpt_header = payload.get('header_template', '')\n    rpt_body = payload.get('body_template', '')\n    combined_tpl = f\"{rpt_header}\\n{rpt_body}\"\n    compiled = jinja_env.from_string(combined_tpl)\n    ctx_data = {'org': payload.get('org_name', 'N/A'), 'period': payload.get('fiscal_period', 'Q1')}\n    rendered_output = compiled.render(**ctx_data)\n    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":"from jinja2 import SandboxedEnvironment\nfrom flask import Flask, request, jsonify\nimport re\n\napp = Flask(__name__)\njinja_env = SandboxedEnvironment()\n\n# Allowlist: only permit simple variable placeholders and basic filters\nSAFE_TEMPLATE_PATTERN = re.compile(\n    r'^[^{]*(?:\\{\\{\\s*[a-zA-Z_][a-zA-Z0-9_.]*(?:\\s*\\|\\s*[a-zA-Z_]+)?\\s*\\}\\}[^{]*)*$'\n)\n\ndef validate_template(template_str, field_name):\n    \"\"\"Validate that a template string only contains safe placeholders.\"\"\"\n    if not isinstance(template_str, str):\n        raise ValueError(f\"'{field_name}' must be a string.\")\n    if len(template_str) > 5000:\n        raise ValueError(f\"'{field_name}' exceeds maximum allowed length.\")\n    # Block dangerous patterns: attribute/item access chains, underscores (dunder access),\n    # and executable constructs like {% %} blocks\n    if '{%' in template_str or '%}' in template_str:\n        raise ValueError(f\"'{field_name}' contains disallowed template logic blocks.\")\n    if '__' in template_str:\n        raise ValueError(f\"'{field_name}' contains disallowed dunder access.\")\n    if not SAFE_TEMPLATE_PATTERN.match(template_str):\n        raise ValueError(f\"'{field_name}' contains disallowed template expressions.\")\n    # Block bracket notation access (e.g., config['key'] or obj[\"key\"])\n    if re.search(r'\\[', template_str):\n        raise ValueError(f\"'{field_name}' contains disallowed bracket access.\")\n\n\n@app.route('/api/v1/report/preview', methods=['POST'])\ndef render_report_preview():\n    payload = request.json\n    if not payload or not isinstance(payload, dict):\n        return jsonify({'error': 'Invalid JSON payload.'}), 400\n\n    rpt_header = payload.get('header_template', '')\n    rpt_body = payload.get('body_template', '')\n\n    try:\n        validate_template(rpt_header, 'header_template')\n        validate_template(rpt_body, 'body_template')\n    except ValueError as e:\n        return jsonify({'error': str(e)}), 400\n\n    combined_tpl = f\"{rpt_header}\\n{rpt_body}\"\n\n    try:\n        compiled = jinja_env.from_string(combined_tpl)\n        ctx_data = {\n            'org': payload.get('org_name', 'N/A'),\n            'period': payload.get('fiscal_period', 'Q1')\n        }\n        rendered_output = compiled.render(**ctx_data)\n    except Exception:\n        return jsonify({'error': 'Template rendering failed.'}), 400\n\n    return jsonify({'preview': rendered_output})"}