En resumen: el XSS en PHP se evita codificando cada salida de forma adecuada a su contexto: htmlspecialchars() para HTML, json_encode() con opciones de protección para JavaScript, URL verificadas para los enlaces. Las plantillas con escapado automático le quitan buena parte de ese trabajo. Una Content Security Policy y las cookies HttpOnly limitan el daño si aun así queda alguna vulnerabilidad.
Por qué ahora conviene revisar también el cross-site scripting
En octubre de 2026 la atención se centra en PHP2Shell, dos vulnerabilidades críticas en el intérprete de PHP para las que hay parches desde el 21/10/2026. Muchas empresas aprovechan para revisar sus aplicaciones web. Es una buena ocasión para mirar también las debilidades del código propio que ninguna actualización de PHP corrige.
El cross-site scripting, XSS para abreviar, es una de ellas. El OWASP Top 10 incluye XSS dentro de la categoría de inyección. A diferencia de una vulnerabilidad en el servidor, aquí el ataque se dirige contra los visitantes y usuarios de su sitio web, por ejemplo contra los clientes de la tienda o el personal del backend.
Las consecuencias pueden ser considerables: sesiones de administradores secuestradas, formularios manipulados que envían datos a terceros o contenidos falsificados en su sitio web. Si se toma el control de una cuenta de administrador, puede llegarse hasta el control total de la aplicación.
¿Cómo surge el XSS en las aplicaciones PHP?
El XSS surge cuando una aplicación muestra en una página, sin verificar, datos de una fuente no fiable. El navegador ya no puede distinguir entonces entre sus contenidos y el código introducido a escondidas, y ejecuta ambos.
Se distinguen tres formas. En el XSS reflejado, el contenido procede directamente de la petición actual, por ejemplo de un término de búsqueda en la URL. En el XSS almacenado, está en la base de datos, por ejemplo en un comentario o en un campo de perfil, y afecta a cualquiera que abra la página. El XSS basado en DOM surge en el JavaScript del navegador, cuando los scripts escriben en la página valores procedentes de la URL.
No son fiables todos los datos que no figuran fijos en el código: campos de formulario, parámetros de URL, cookies, nombres de archivo, respuestas de interfaces externas y contenidos de la propia base de datos que en algún momento vinieron de fuera.
Codificar correctamente la salida según el contexto
La regla más importante es: codificar al mostrar, y de forma adecuada al lugar donde acaba el valor. Para el contenido HTML y los atributos entre comillas, htmlspecialchars() es la herramienta indicada. Una pequeña función auxiliar evita que se olviden las opciones:
<?php
function e(?string $wert): string
{
return htmlspecialchars($wert ?? '', ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}
?>
<h2><?= e($produkt['name']) ?></h2>
<input type="text" name="suche" value="<?= e($_GET['q'] ?? '') ?>">
Otros contextos requieren otros medios:
| Contexto | Método seguro |
|---|---|
| Contenido HTML, atributo entre comillas | htmlspecialchars() con ENT_QUOTES y UTF-8 |
| Datos para JavaScript | json_encode() con JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_HEX_QUOT |
| Parámetro en una URL | rawurlencode() para cada valor |
| URL completa a partir de una entrada del usuario | Comprobar el esquema, admitir solo https y http, después htmlspecialchars() |
| Atributos sin comillas, controladores de eventos, CSS | evitar, no insertar valores ahí |
Para los datos que necesita un script, un atributo de datos o un bloque JSON suele ser el camino más limpio:
<?php
$daten = json_encode($konfiguration, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<div id="app" data-konfig="<?= e($daten) ?>"></div>
En JavaScript, el valor se lee después mediante el atributo de datos y los contenidos se establecen con textContent en lugar de innerHTML.
Plantillas, depuración de HTML y frameworks
Los motores de plantillas como Twig y Blade codifican las salidas automáticamente por defecto. Eso excluye gran parte de los errores típicos. Se vuelve peligroso donde ese comportamiento se desactiva a propósito, por ejemplo con el filtro raw en Twig o con la sintaxis {!! !!} en Blade. Esos puntos deberían ser escasos en el código y revisarse de forma específica en cada revisión.
Si su aplicación debe admitir deliberadamente HTML de los usuarios, por ejemplo formatos en comentarios o en un sistema editorial, no basta con codificar, porque el HTML precisamente debe mostrarse. Entonces necesita una biblioteca de depuración contrastada como HTML Purifier, que solo deja pasar una lista fija de elementos y atributos permitidos. Los filtros propios con expresiones regulares resultan, según la experiencia, incompletos.
Revise especialmente las áreas de backend. El XSS en vistas que solo ven los administradores suele subestimarse, pero afecta justamente a las cuentas con más permisos.
Encontrar XSS en el código existente de PHP
En aplicaciones crecidas, la primera pregunta no suele ser cómo codificar correctamente, sino dónde se ha olvidado hasta ahora. Un comienzo pragmático es buscar los puntos donde se muestran variables directamente:
# Salidas directas de datos de la petición
grep -rnE "(echo|print|<\?=)\s*\\\$_(GET|POST|REQUEST|COOKIE|SERVER)" src/ templates/ --include="*.php"
# Escapado automático desactivado en Twig y Blade
grep -rnE "\|raw|\{!!" templates/ resources/views/
Las coincidencias son tareas de revisión, no hallazgos definitivos. El XSS almacenado solo lo encuentra en parte una búsqueda así, porque allí los valores proceden de la base de datos. Revise por eso además todas las vistas que muestran contenidos de usuarios: comentarios, perfiles, notas de pedidos, tickets de soporte. En los CMS y las tiendas, los plugins y temas de terceros forman parte expresamente de ello, porque allí reside buena parte de la lógica de salida.
Segunda línea de defensa: CSP y cookies seguras
Incluso con un código cuidadoso puede pasarse por alto algún punto. Dos medidas limitan entonces el daño. Una Content Security Policy permite al navegador solo scripts de fuentes definidas y prohíbe los scripts incrustados, salvo que se autoricen expresamente:
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
Introduzca una política así de forma gradual, primero en el modo de solo informes mediante Content-Security-Policy-Report-Only, porque las integraciones existentes de servicios de análisis o de pago suelen tener que añadirse. Otras cabeceras las describe la guía Cabeceras de seguridad para sitios web.
Además, las cookies de sesión deberían llevar el atributo HttpOnly, para que los scripts no puedan leerlas. Cómo proteger las sesiones en conjunto se explica en la guía Proteger las sesiones en PHP. Una debilidad relacionada, del lado de la base de datos, se trata en Evitar la inyección SQL en PHP.
¿Cómo ayuda PHP SOS en concreto?
- Revisamos las plantillas y las salidas de su aplicación en busca de codificación ausente o incorrecta.
- Valoramos los hallazgos según su accesibilidad y las cuentas afectadas y mostramos correcciones concretas.
- Introducimos una Content Security Policy de forma gradual, sin perturbar las funciones de la tienda ni de pago.
- Volvemos a probar tras la corrección y documentamos el resultado.
Una revisión independiente la describe el servicio Auditoría de seguridad y prueba de penetración.
Preguntas frecuentes
¿Basta htmlspecialchars contra XSS?
Para texto normal en el contenido HTML y en atributos entre comillas, sí, si indica ENT_QUOTES y el juego de caracteres UTF-8. En JavaScript, en URL o en CSS rigen otras reglas y allí hace falta una codificación propia y adecuada.
¿Debo filtrar las entradas al guardarlas o al mostrarlas?
Codifique al mostrar, según el destino. Las entradas debería comprobarlas al recibirlas en cuanto a plausibilidad, por ejemplo longitud y formato. Quien solo filtra al guardar pasa por alto datos de otras fuentes y dificulta el uso posterior en otros contextos.
¿Qué aporta una Content Security Policy?
Una Content Security Policy establece desde qué fuentes puede el navegador cargar y ejecutar scripts. Puede limitar de forma notable las consecuencias de una vulnerabilidad XSS pasada por alto, pero no sustituye a una codificación correcta.
¿Cómo permito formatos como negrita o enlaces en los comentarios?
Utilice una biblioteca de depuración de HTML contrastada con una lista blanca de elementos y atributos permitidos. Las expresiones regulares propias para eliminar scripts son propensas a errores y no deberían usarse.