SSTI (Server-Side Template Injection): qué es, cómo se detecta y cómo se corrige
· 12 min de lectura
Aprende qué es la inyección de plantillas del lado del servidor, cómo se detecta con expresiones matemáticas, cómo identificar el motor (Jinja2, Twig, ERB, FreeMarker), por qué puede terminar en ejecución de código en un laboratorio autorizado y cómo se corrige sin construir plantillas con datos del usuario.
Casi ninguna web escribe su HTML a mano en cada respuesta. Usa un motor de plantillas: un archivo con huecos, como «Hola, {{ nombre }}», que el servidor rellena con datos antes de enviar la página. Jinja2 en Python, Twig en PHP, ERB en Ruby o FreeMarker en Java son algunos de los más usados. Bien usados, son seguros y cómodos.
El problema aparece cuando el programador construye la propia plantilla con datos del usuario. Entonces el usuario ya no rellena un hueco: escribe la plantilla. Y como las plantillas pueden hacer cálculos, acceder a objetos y, en muchos motores, llamar a funciones del lenguaje, el resultado puede ser ejecución de código en el servidor. Eso es la Server-Side Template Injection (SSTI). En esta lección verás cómo se reconoce, cómo se identifica el motor, por qué es tan grave y cómo se corrige.
Practica estas técnicas solo en sistemas propios o en entornos autorizados, como los laboratorios de whoami-labs. Inyectar plantillas en un servidor ajeno sin permiso es un delito.
Qué es una SSTI#
Una SSTI ocurre cuando una aplicación mete la entrada del usuario dentro del texto de una plantilla y después le pide al motor que la procese. El motor no sabe qué parte escribió el programador y qué parte vino del usuario: lo evalúa todo.

La diferencia es sutil pero decisiva. Si la plantilla es fija y el dato se pasa como variable, el motor lo trata como texto y, como mucho, lo codifica para HTML. Si el dato se concatena a la plantilla, cualquier sintaxis del motor que contenga, como llaves dobles, se interpreta como instrucciones.
Por eso la SSTI se parece al XSS en lo que ves al principio (tu texto aparece en la página), pero es mucho más grave: el XSS se ejecuta en el navegador de la víctima, mientras que la SSTI se ejecuta en el servidor.
Por qué ocurre: la causa raíz#
Imagina una aplicación Flask que personaliza un mensaje de bienvenida:
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route("/hola")
def hola():
nombre = request.args.get("nombre", "invitado")
plantilla = "<h1>Hola, " + nombre + "</h1>" # el dato forma parte de la plantilla
return render_template_string(plantilla)
Con ?nombre=Ana la página muestra «Hola, Ana». Pero con ?nombre={{7*7}} muestra «Hola, 49»: el motor ha calculado la expresión. El usuario está programando la plantilla.
El mismo error aparece en otros lenguajes. En PHP con Twig:
// Vulnerable: la plantilla se crea a partir del texto del usuario
$twig = new \Twig\Environment(new \Twig\Loader\ArrayLoader([]));
$plantilla = $twig->createTemplate("Hola " . $_GET['nombre']);
echo $plantilla->render([]);
Suele colarse en funciones que parecen inofensivas: correos personalizables por el administrador, mensajes de error con el nombre del usuario, plantillas de notificaciones editables o generadores de documentos.
Motores y variantes#

Cada motor tiene su propia sintaxis, y eso es justo lo que te permite identificarlo:
| Motor | Lenguaje y framework habitual | Sintaxis de expresión | Prueba que lo delata |
|---|---|---|---|
| Jinja2 | Python (Flask) | Llaves dobles | 7*'7' devuelve 7777777 |
| Twig | PHP (Symfony) | Llaves dobles | 7*'7' devuelve 49 |
| ERB | Ruby (Rails) | Etiquetas con porcentaje e igual | Las llaves dobles salen literales |
| FreeMarker | Java | Dólar y llaves | Las llaves dobles salen literales |
| Handlebars, Mustache | JavaScript y otros | Llaves dobles | Sin lógica: no calculan 7*7 |
La diferencia entre Jinja2 y Twig es un buen ejemplo de lo útil que es conocer el lenguaje de fondo. En Python, multiplicar una cadena por un número la repite, así que Jinja2 devuelve siete sietes. Twig, en cambio, convierte la cadena en número y devuelve 49.
SSTI con salida y SSTI ciega#
Lo normal es ver el resultado en la página, como en el ejemplo. Pero a veces la plantilla se procesa y se usa en otro sitio, por ejemplo en un correo que nunca ves. En ese caso hay que confirmar la inyección por otros medios: un error que cambia, una diferencia de tiempo o una petición saliente hacia un servidor que controlas en el laboratorio.
Inyección en contexto de código#
A veces tu dato no cae en el texto de la plantilla, sino dentro de una expresión ya abierta, por ejemplo como nombre de variable. Ahí {{7*7}} no funciona, pero sí cerrar la expresión y abrir otra, o provocar un error de sintaxis que delate el motor.
Cuándo es explotable#
Para que haya SSTI tienen que darse dos condiciones:
- El dato del usuario llega al texto de la plantilla, no a sus variables.
- El motor evalúa expresiones. Los motores sin lógica, como Mustache, reducen mucho el riesgo, porque solo sustituyen variables.
La gravedad depende de lo que el motor deja hacer. En los motores que exponen objetos del lenguaje, como Jinja2, ERB o FreeMarker con su configuración por defecto, el atacante puede navegar desde un objeto cualquiera hasta funciones del sistema, y eso termina en ejecución de comandos. En motores con sandbox bien configurado, el impacto puede quedarse en lectura de datos de la plantilla, aunque los sandboxes también han tenido fallos.
Cómo se detecta#
Pruebas manuales#
La técnica se basa en enviar expresiones que solo un motor de plantillas calcularía:
- Localiza los campos que se reflejan en la respuesta, incluidos parámetros ocultos o poco documentados.
- Envía una mezcla de caracteres especiales de plantillas para provocar un error: si aparece un error del motor, ya sabes cuál es.
- Envía expresiones matemáticas en varias sintaxis y busca el resultado calculado.
- Si una funciona, usa pruebas diferenciales para afinar el motor.
- Confirma con algo inocuo, como mostrar el valor de una variable de la plantilla.
Estas son las pruebas clásicas de detección:
{{7*7}}
${7*7}
<%= 7*7 %>
#{7*7}
${{7*7}}
{{7*'7'}}
| Si ves | Probablemente es | Siguiente prueba |
|---|---|---|
| 49 con llaves dobles | Jinja2 o Twig | Multiplicar por una cadena para distinguirlos |
| 49 con dólar y llaves | FreeMarker u otro motor Java | Buscar errores con el nombre del motor |
| 49 con etiquetas de porcentaje | ERB (Ruby) | Confirmar con una expresión de Ruby inocua |
| Error con traza del motor | El que diga la traza | Leer la versión y el framework |
| Tu texto tal cual | No se evalúa o el dato va a una variable | Probar en otros parámetros |
Una precaución importante: {{7*7}} que devuelve 49 en el navegador puede ser un framework de JavaScript del lado del cliente, como AngularJS. Eso es inyección de plantillas en el cliente, que se parece más a un XSS. Mira siempre la respuesta del servidor en Burp, no la página ya renderizada.
Herramientas#
- Burp Suite: Repeater para probar sintaxis rápido; su escáner detecta muchos casos de SSTI.
- ffuf o Arjun: para descubrir parámetros ocultos que se reflejan, como en el laboratorio OCTOPUS, donde la clave es un parámetro no documentado.
- tplmap y SSTImap: herramientas especializadas que detectan el motor y automatizan pruebas; úsalas solo en laboratorios.
- Revisión de código: busca
render_template_string,Template(...)ocreateTemplatecon datos del usuario concatenados, y en RubyERB.newcon texto que venga de la petición.
Explotación paso a paso en un laboratorio#
Vamos a recorrer el razonamiento con una tienda en Ruby on Rails, el escenario del laboratorio OCTOPUS. El buscador funciona bien, pero hay un parámetro que nadie documentó y que la aplicación procesa como plantilla ERB.
Paso 1: descubrir el parámetro oculto#
Compara el tamaño de las respuestas al probar nombres de parámetros. Un parámetro que existe suele cambiar la respuesta:
ffuf -u "http://IP_DEL_LAB/buscar?q=martillo&FUZZ=wl7x9q" \
-w /usr/share/wordlists/dirb/common.txt -fs TAMAÑO_NORMAL
Cuando encuentres uno cuya respuesta cambia de tamaño, comprueba si wl7x9q aparece reflejado en la página.
Paso 2: detectar la evaluación#
Envía las expresiones de prueba en ese parámetro, codificadas para URL. Si la sintaxis de ERB devuelve 49 y las llaves dobles salen tal cual, el motor es ERB:
GET /buscar?q=martillo&PARAMETRO=%3C%25%3D%207*7%20%25%3E HTTP/1.1
Host: IP_DEL_LAB
Paso 3: confirmar con algo inocuo#
Antes de pensar en el impacto, confirma que estás ejecutando Ruby con una expresión que no cambia nada, por ejemplo la versión del intérprete:
<%= RUBY_VERSION %>
Si aparece la versión de Ruby en la respuesta, ya no estás ante un simple reflejo: el servidor ejecuta el código Ruby que envías.
Paso 4: entender el alcance#
En ERB, lo que va entre esas etiquetas es Ruby completo, así que la plantilla puede hacer lo mismo que el proceso de la aplicación: leer archivos a los que tenga acceso o ejecutar programas del sistema. En Jinja2 el camino es más largo, porque hay que llegar desde los objetos de la plantilla hasta las clases de Python, pero el resultado es el mismo. En el laboratorio, este es el punto en el que consigues ejecución de código y acceso inicial a la máquina con el usuario del servicio web, como indica el objetivo de OCTOPUS.
Fíjate en que todo depende de un solo error: el dato del usuario se trató como plantilla. Con la plantilla fija y el dato como variable, la misma petición solo habría mostrado el texto tal cual.
Qué puede lograr un atacante#
| Impacto | Ejemplo real |
|---|---|
| Ejecución remota de código | Ejecutar comandos con el usuario del servidor web |
| Lectura de archivos | Código fuente, archivos de configuración y claves |
| Robo de secretos de la aplicación | La clave secreta de Flask, que permite falsificar sesiones |
| Lectura de datos de la plantilla | Variables internas, objetos de configuración o datos de otros usuarios |
| Movimiento lateral | Usar el servidor comprometido para atacar la red interna |
| Denegación de servicio | Expresiones que consumen mucha CPU o memoria |
Cómo se corrige#
La regla de oro: la plantilla la escribe el programador; el usuario solo aporta datos, que se pasan como variables.
Plantillas fijas y datos como variables#
# Vulnerable: el nombre forma parte de la plantilla
return render_template_string("<h1>Hola, " + nombre + "</h1>")
# Seguro: la plantilla es fija y el nombre se pasa como variable
return render_template_string("<h1>Hola, {{ nombre }}</h1>", nombre=nombre)
# Mejor aún: la plantilla vive en un archivo del proyecto
return render_template("hola.html", nombre=nombre)
En PHP con Twig, lo mismo:
// Seguro: plantilla fija, dato como variable
echo $twig->render('hola.html.twig', ['nombre' => $_GET['nombre']]);
Si los usuarios deben personalizar plantillas#
Hay casos legítimos, como un administrador que edita el texto de los correos. Entonces:
- Usa un motor sin lógica, como Mustache o Handlebars, que solo sustituye variables y no puede llamar a funciones.
- O usa el sandbox del motor: Jinja2 tiene
SandboxedEnvironmenty Twig tiene su extensión Sandbox con listas de etiquetas, filtros y métodos permitidos. Configúralos con lo mínimo imprescindible. - Limita quién puede editar plantillas y trata ese permiso como uno de administración del servidor.
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
plantilla = env.from_string(texto_del_administrador)
salida = plantilla.render(nombre=cliente.nombre)
Defensa en profundidad#
- Ejecuta la aplicación con un usuario sin privilegios y en un contenedor con lo mínimo, para que una ejecución de código no sea el control total.
- No muestres trazas de error en producción: delatan el motor y la versión.
- Mantén los motores actualizados, porque los sandboxes han tenido fallos corregidos en versiones posteriores.
- Revisa el código buscando cualquier función que cree plantillas a partir de cadenas.
Errores comunes al defenderse#
- Filtrar las llaves. Cada motor tiene varias sintaxis y codificaciones; los filtros de caracteres se saltan.
- Confiar ciegamente en el sandbox. Reduce el riesgo, pero se han publicado escapes en distintos motores. Es una capa, no la solución.
- Pensar que el autoescape protege. El escape para HTML evita el XSS en las variables, pero no impide que el motor evalúe una plantilla construida con datos del usuario.
- Olvidar las funciones de administración. Las plantillas editables por administradores también son un riesgo si su cuenta se compromete.
- Confundirla con un XSS. Si se corrige como un XSS, codificando la salida, la ejecución en el servidor sigue ahí.
Preguntas frecuentes#
¿Qué diferencia hay entre SSTI y XSS?#
En las dos tu texto aparece en la página, pero la SSTI se evalúa en el servidor, antes de enviar la respuesta, y puede ejecutar código en él. El XSS se ejecuta en el navegador de quien visita la página.
¿Si veo 49 ya tengo ejecución de código?#
No necesariamente. Demuestra que el motor evalúa tu entrada, pero el impacto depende del motor y de su configuración. Con un sandbox estricto o un motor limitado, quizá solo puedas leer datos de la plantilla.
¿Por qué Jinja2 y Twig dan resultados distintos con la misma prueba?#
Porque heredan las reglas de su lenguaje: en Python, multiplicar una cadena por un número la repite, y en Twig la cadena se convierte en número. Esa diferencia es útil para identificar el motor.
¿Los motores sin lógica son totalmente seguros?#
Son mucho más seguros frente a la SSTI porque no pueden ejecutar funciones. Aun así, conviene que las variables se escapen para HTML y que nadie pueda registrar funciones auxiliares peligrosas.
¿Es legal probar expresiones de plantillas en una web?#
Solo en sistemas propios, en laboratorios o en programas de bug bounty que lo permitan. Aunque sea solo {{7*7}}, hacerlo en un sistema ajeno sin autorización puede considerarse un acceso no permitido.
Resumen#
- Una SSTI ocurre cuando la entrada del usuario forma parte del texto de una plantilla y el motor la evalúa.
- Se detecta con expresiones matemáticas en distintas sintaxis y se identifica el motor por cómo responde.
- En motores que exponen el lenguaje (Jinja2, ERB, FreeMarker) puede llegar a ejecución de código en el servidor.
- Se corrige con plantillas fijas y datos como variables; si hay que permitir plantillas de usuario, con motores sin lógica o un sandbox bien configurado.
- Ejecutar la aplicación con mínimos privilegios limita el daño si algo falla.
Fuentes y lecturas recomendadas#
- PortSwigger Web Security Academy: Server-side template injection
- OWASP Web Security Testing Guide: Testing for Server-side Template Injection
- MITRE CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine
- Documentación de Jinja2: Sandbox
- Documentación de Twig: Sandbox extension