Web
SSTI: server-side template injection (Twig/Blade)
When user text is passed straight into a template engine, it can be executed as code instead of rendered as text. How SSTI happens, how to test for it, and how to prevent it in Twig/Blade.
· 6 min read

Almost every developer has heard of SQL injection, but template injection (SSTI — Server-Side Template Injection) is less well known, even though the impact can be just as severe: an attacker executing their own code on the server (RCE).
Where the problem comes from
Template engines like Twig, Blade, Jinja2 or FreeMarker do two things: output static text and evaluate the expressions inside {{ }}. If an application processes user-supplied text as the template source itself, an attacker sends template syntax instead of plain text, and the engine executes it as code.
A simple example — building a message like this:
$template = "Hello, " . $request->name . "!";
return Blade::render($template);
Here the user's text is re-rendered as the template itself. If {{ phpinfo() }} or {{ system('id') }} is sent instead of a name, Blade treats it as an expression and executes it. The same mistake appears in Twig: passing user text through $twig->createTemplate($userInput)->render() is a classic SSTI entry point. For comparison, an ordinary call like view('welcome', ['name' => $request->name]) is safe, because name is only a value there, not a template.
Why this leads to RCE
An ordinary template expression can be a math operation or a variable reference, but most template engines also allow calling object methods. In Twig and Blade, PHP itself runs underneath, so a well-crafted payload can reach the filesystem, environment variables, or even command-execution functions. As a result, SSTI often turns into full control of the server — one of the most severe forms of the injection category.
A template engine is a small programming language. Never feed it user text as a "template" — only as "data."
How to test for it
The simplest test is to send a math expression and see what comes back:
name=${7*7}
name={{7*7}}
name=#{7*7}
name=<%= 7*7 %>
If 49 shows up in the response instead of plain text, the submitted value was executed as a template. Which syntax works also hints at the engine: {{ }} is Twig/Jinja2/Blade style, ${ } is FreeMarker/Velocity, <%= %> is ERB.
List every place where user text could end up being used as a "template" — form fields, email templates, PDF generators, notification text — and send this same probe to each:
curl -s https://your-domain.uz/contact \
--data-urlencode "message={{7*7}}" | grep -o "49"
How to fix it
1. Never render user text as a template
The most reliable fix is to never use user-supplied text as a template source at all. Templates should be files written by developers and kept under version control; user data should only ever be passed as variable values:
// Dangerous: user text is the template source
return Blade::render($request->input('template'), $data);
// Correct: user text is only a value
return view('messages.welcome', ['name' => $request->input('name')]);
2. If a dynamic template is genuinely needed — sandbox it
Sometimes a business requirement forces you to let a user write their own template (an email builder, for example). In that case, use Twig's SandboxExtension, which blocks calls to functions and methods that are not explicitly allowed. Blade has no equivalent sandbox, so avoid rendering a user-supplied template through Blade at all; instead use a restricted variable substitution, such as manually replacing simple placeholders like {{ name }}.
3. Output escaping is still required
SSTI and XSS are related problems: even after the template injection is closed, output still needs to be escaped correctly. In Blade, {{ }} escapes automatically while {!! !!} does not — never output user text through {!! !!}.
Why this mistake keeps recurring
SSTI often shows up disguised as "convenience": a developer wants to let an admin panel edit an email template, a notification message, or a report heading. The idea itself isn't bad — the problem is that "editable" often gets confused with "rendered as a template." Even if text can be entered through an admin panel, that doesn't mean it should automatically turn into template language. If an admin panel has a "template" field that later gets processed through Blade::render(), that's exactly this mistake, just with the assumption that only an administrator will use it. But if the admin account is compromised, or there's an insider threat, the same path can lead to full control of the server. So the assumption that "only a trusted user reaches this" is not, by itself, sufficient protection.
Checklist after the fix
- Probes like
{{7*7}}were sent to every place where user text could act as a template, and the response stayed unchanged - User text is never passed directly into functions such as
Blade::render()orTwig::createTemplate() - A sandbox or restricted placeholder mechanism is used wherever a dynamic template is genuinely required
- "Raw" output is reserved only for trusted sources
Summary
SSTI isn't as widely known as SQL injection, but the impact is comparable — often full control of the server. The rule is simple: give the template engine only templates written by developers, and treat user text purely as data. This type of injection is also covered in the OWASP Top 10.
Want to know whether your application has SSTI or other injection vulnerabilities? Request a security scan — the Claude AI agent will review your codebase, and our specialist will confirm the findings and help close them.


