Server-side Template Injection (SSTI) is a vulnerability that stems from the server rendering a template containing raw, unsanitized user input. While SSTI can be mistaken for ‘just’ cross-site scripting (XSS) executing arbitrary code in the browser, the larger danger lies in the relative ease of gaining remote code execution on the server.

Flask/Jinja2

That definition of SSTI is still a little vague though, so what does it really look like? Let’s use Jinja2 in a Flask App as an example.

When rendering a response to the user, if the template (or underlying string) is composed with raw, unsanitized user input prior to being rendered then there is a good chance that it is vulnerable to SSTI. The vulnerability would most likely come in the form of string concatenation or string substitution.

String Concatentation1

@app.route("/page")
def page():
    name = request.values.get('name')
    return render_template_string('Hello ' + name + '!').render()

String Substitution/Formatting2

@app.errorhandler(404)
def page_not_found(e):
    template = '''<div class="center-content error">
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>
    </div>''' % (request.url)
    return render_template_string(template), 404

In both of those cases, the user input is injected into the template prior to being rendered. Tim Tomes does a great job detailing how to exploit SSTI in his blog post so I’m not going to rehash it here. The TL;DR is that SSTI in these scenarios leads trivially to RCE.

Okay, so that’s a problem, but how do you fix this? Ditch Jinja and Flask? Nah, there’s an easier way. Render with context! Does that sound a bit familiar? It should! Rendering with context is like using a prepared statement in SQL queries. When done properly it helps protect your application against this kind of attack.

    context = {
        'name': request.values.get('name')
    }
    return render_template_string('Hello {{ name }}!', **context)
    template = '''<div class="center-content error">
        <h1>Oops! That page doesn't exist.</h1>
        <h3>{{ url }}</h3>
    </div>'''
    context = {
        'url': request.url
    }
    return render_template_string(template, **context), 404

Or ideally, render via a static template file

    resp_code = 200
    context = {
        'name': name,
        'url': request.url
    }
    return render_template('my_template.html', **context), resp_code

SSTI shares many similarities with SQL injection. SQL injection vulnerabilities commonly occur from both string concatenation and improper use of prepared statements. In the case of SSTI, the analogous mistake to SQL injection’s improper use of prepared statements is passing a template with direct user input into render_template_string instead of using a variable within the template and passing the user input in via context.

The bottom line is that if you’re rendering content with user input, make sure that you use a variable in the template and render it with context to safely include user input on the resulting page.

What does an exploit look like?

If you’re not already familiar with SSTI in Flask/Jinja2, you may be wondering what an exploit of this would look like. Tim Tomes’ post does a very detailed walkthrough of it all, but I’ll try to provide a condensed version here.

# Is it vulnerable?
{{7*7}}

# How to we get to the root, Object class?  This also implies which version of python is running
{{ ''.__class__.__mro__ }}

# Use the root object class to then get the entire class tree via its subclasses
{{ ''.__class__.__mro__[1].subclasses__() }}

# Find an interesting subclass such as <type 'file'> or <class 'subprocess.Popen'> and interact with the host
# subprocess.Popen example
{{ ''.__class__.__mro__[1].__subclasses__()[213]('/usr/bin/whoami', shell=True, stdout=-1).communicate() }}

References