# Jinja2 Server-Side Template Injection via Untrusted render_template_string()

Language: Python
Severity: Critical
CWE: CWE-1336

## Source
10

## Flow
10-12

## Sink
12

## Vulnerable Code
```python
from flask import Flask, request, render_template_string
import boto3

app = Flask(__name__)
s3_client = boto3.client('s3')

@app.route('/cloud/bucket-report')
def generate_bucket_report():
    bucket_name = request.args.get('bucket', 'default-bucket')
    report_format = request.args.get('format', 'Bucket: {{bucket}}')
    object_count = s3_client.list_objects_v2(Bucket=bucket_name).get('KeyCount', 0)
    rendered_report = render_template_string(report_format, bucket=bucket_name, count=object_count)
    return rendered_report, 200, {'Content-Type': 'text/plain'}
```

## Explanation

The application accepts user-controlled input from the 'format' parameter (line 10) and passes it directly to render_template_string() (line 12) without validation or sanitization. This allows attackers to inject arbitrary Jinja2 template expressions that execute server-side, leading to Server-Side Template Injection (SSTI) which can result in remote code execution.

## Remediation

The fix eliminates the SSTI vulnerability by removing render_template_string() entirely and replacing it with a whitelist of predefined safe format strings. Users now select from approved format keys instead of supplying arbitrary template strings, and Python's str.format() is used for safe string interpolation with HTML-escaped values.

## Secure Code
```python
from flask import Flask, request, escape
import boto3

app = Flask(__name__)
s3_client = boto3.client('s3')

PREDEFINED_FORMATS = {
    'simple': 'Bucket: {bucket}',
    'detailed': 'Bucket: {bucket} | Object Count: {count}',
    'summary': 'Report for "{bucket}": {count} objects found.'
}

@app.route('/cloud/bucket-report')
def generate_bucket_report():
    bucket_name = request.args.get('bucket', 'default-bucket')
    format_key = request.args.get('format', 'simple')
    
    if format_key not in PREDEFINED_FORMATS:
        return 'Invalid format. Available formats: ' + ', '.join(PREDEFINED_FORMATS.keys()), 400, {'Content-Type': 'text/plain'}
    
    object_count = s3_client.list_objects_v2(Bucket=bucket_name).get('KeyCount', 0)
    
    report_template = PREDEFINED_FORMATS[format_key]
    rendered_report = report_template.format(
        bucket=escape(bucket_name),
        count=object_count
    )
    return rendered_report, 200, {'Content-Type': 'text/plain'}
```
