Prevent Server-Side Template Injections

Keep your app safe from SSTI attacks.

Learn about the Server-Side Template Injection (SSTI) vulnerability, and discover how to prevent it.

OWASP includes SSTI in the Top 10 Web Application Security Risks (2021) and lists it under Injection.

Description

A server-side template injection allows an attacker to execute server-side commands by injecting malicious data into a template. A vulnerability occurs when a threat actor embeds invalid user input in a template engine. This can result in remote code execution (RCE).

For example, depending on the IP address from which a user accesses a site, an injection may look like this:

<h1>Welcome to Example Project!</h1>
<u>You're accessing this page from a remote address: {{ip}}</u>

The injection passes a remote address into the {{ip}} variable while reusing the rest of the HTML code for every request it receives.

SSTI may have a devastating effect. It generally results in remote code execution by controlling the backend server. Even if the attacker doesn’t execute any code, they may still access sensitive server data.

Attack Scenarios

Let’s see some example attack scenarios.

Twig Template

This code example illustrates an SSTI vulnerability in the getFilter() function that uses call_user_func($callback, $name). From the HTTP GET request, the server retrieves and executes the name parameter:

public function getFilter($name)
{
        [snip]
        foreach ($this->filterCallbacks as $callback) {
        if (false !== $filter = call_user_func($callback, $name)) {
            return $filter;
        }
    }
    return false;
}

$output = $twig->render($_GET['name'],  array("first_name" => $user.first_name) );

Analysis

In this example, the user controls the template content using the name GET parameter rather than a value passed into it.

Impact

An attacker can execute an arbitrary shell command by registering exec as a filter callback and then calling getFilter().

They can use the env attribute of the Twig_Environment object to run system commands.

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

Prevention

To prevent this, pass a name into the template, as in the following example:

$output = $twig->render("Dear {name},", array("first_name" => $user.first_name) ); 

Jinja2

This Jinja2 code contains a vulnerability. The variable name that a user passes is concatenated to the template string.

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

Analysis

When you send an HTTP GET request, the page function accepts the name parameter and renders an HTML response with the content of the variable.

Impact

Improper use of templates may lead to both Cross-Site Scripting (XSS) and SSTI vulnerabilities. The following code block is an example of a malicious input through the name parameter.

{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}

Bypassing Denylists

Attackers may circumvent the denylists that you configured.

@app.route("/page")
denylist = ["__class__",request[request.]
def page():
    name = request.values.get('name')
    for bad_string in denylist:
        if  bad_string in name:
            return "HACKING ATTEMPT {}".format(bad_string), 400
        else:
            output = Jinja2.from_string('Hello ' + name + '!').render()
            return output    

Analysis

A threat actor can bypass __class__ and retrieve the value of a new GET parameter using request.args.param. Because we included request.args.param in the denylist, we can use a native Jinja2 function | attr().

Impact

An attacker can pass the following input to the server using the name parameter.

{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}

Prevention

Pass the name variable to the template context.

@app.route("/page")
def page():
    name = request.values.get('name')
    output = Jinja2.from_string('Hello {{name}}!').render(name = name)
    return output

Best Practices

A template engine allows you to streamline the process of updating and maintaining your application.

For example, designers or integrators can autonomously make a design change without touching the code flow. Their changes are independent of the code and logical processing. Similarly, developers can change logical elements and code without affecting data display or breaking the interface.

To prevent an SSTI attack, never allow users to modify or create templates. If you have no other choice, follow our recommendations.

Sanitize Data

Before using a template, identify and remove potentially malicious content.

Make sure to efficiently analyze data that users pass. You can use various methods to do this, such as regular expression tools or allowlists of authorized expressions.

However, there is a drawback to this solution. Configuration errors can put your environment at risk.

Set Up a Sandbox Environment

You can turn off risky modules and features in a closed sandbox environment. This allows you to limit access to other application components.

It may be challenging to create a sandbox environment. Misconfigurations may result in bypassing the environment.

Use Logic-Less Templates

A logic-less template engine separates visual rendering from code interpretation. Mustache is one of the most popular engines.

In Mustache templates, none of the control flows is explicit because data drives all the control. As a result, you can’t integrate your application logic into templates.

Keep the application logic and presentation as separate as possible to reduce the risk of template-based attacks. This allows you to prevent remote code execution (RCE) attacks.

References

Last modified December 18, 2024