htmlspecialchars($text) convierte &, <, >, " y ' en &, <, >, " y '. Llámala con cada dato de entrada del usuario que imprimas en una página, y la entrada se mostrará como texto en lugar de leerse como HTML.
El bloque imprime el mismo comentario dos veces, una sin modificar y otra escapada. Ejecútalo y compara las dos líneas en la pestaña Página; luego escribe tu propio HTML en el formulario, por ejemplo <h1>big</h1> o <img src=x>, y pulsa Show.
En la línea sin modificar el navegador obedece a las etiquetas: la palabra en negrita sale en negrita, y un visitante que escriba <script> consigue que su script se ejecute en el navegador de todos los demás lectores. Ese ataque se llama cross-site scripting (XSS). En la línea escapada los mismos caracteres llegan como <b> y el navegador los dibuja como texto. Cambia a la pestaña Salida para ver las entidades que imprimió realmente PHP.
Qué convierte htmlspecialchars
Cinco caracteres y nada más. Las letras, las tildes y los emojis pasan sin cambios.
& está en la lista porque es el comienzo de toda entidad: si se dejara tal cual, un comentario que menciona < se mostraría como <.
Escapar los atributos, no solo el texto
La entrada del usuario dentro de un atributo necesita escaparse igual. Sin escapar, unas comillas en el valor cierran el atributo y el resto de la entrada se convierte en atributos nuevos. Aquí el "nombre" cuela un atributo style; ejecútalo y mira las dos cajas.
En la caja insegura el navegador ve value="Ada" seguido de un atributo style nuevo, así que la caja se vuelve roja y solo muestra Ada. Un atacante escribiría ahí onfocus="..." en lugar de style, y su código se ejecutaría. En la caja segura cada " se convirtió en ", así que toda la cadena se queda dentro de value y se muestra tal como se escribió.
Desde PHP 8.1 las opciones por defecto son ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, así que las comillas simples también se escapan y los atributos escritos con '...' son seguros. El código antiguo suele pasar ENT_QUOTES a mano, y en PHP 7 y anteriores era obligatorio:
Una función auxiliar corta para las plantillas
Escribir htmlspecialchars($x, ENT_QUOTES, 'UTF-8') decenas de veces en una plantilla es ruidoso, así que la mayoría de los proyectos la envuelven en una función de una letra. Los motores de plantillas como Twig y Blade hacen lo mismo automáticamente con cada {{ $var }}.
El tipo ?string y el ?? '' importan: pasar null a htmlspecialchars() está obsoleto desde PHP 8.1, y tu PHP imprimiría un aviso de obsolescencia por cada usuario sin biografía.
Doble codificación y htmlspecialchars_decode
Si un valor se escapa dos veces, el lector ve las entidades: & se convierte en & la primera vez y en &amp; la segunda, que el navegador muestra como &. Normalmente significa que el valor se escapó al guardarlo y otra vez al imprimirlo. Pasa double_encode: false para dejar intactas las entidades que ya existen, y usa htmlspecialchars_decode() para volver atrás.
La solución real es guardar el texto sin modificar y escapar solo en la salida. double_encode: false es para texto que ya contiene entidades que no creaste tú, como un feed importado.
htmlspecialchars vs htmlentities vs strip_tags
Estas tres se confunden a menudo. El bloque las ejecuta todas con la misma entrada y muestra, para cada una, lo que imprime PHP y lo que hace con ello el navegador:
htmlspecialchars()escapa los cinco caracteres de HTML. Úsala con cualquier texto que imprimas en HTML.htmlentities()convierte ademáséené. Era útil cuando las páginas no eran UTF-8; hoy solo hace más difícil de leer el código fuente.strip_tags()borra las etiquetas y conserva su texto. Sirve para convertir HTML en texto plano (una vista previa de un email, una meta descripción), no para la seguridad: la última fila muestra que un<b>permitido conserva suonclick, y el texto colocado dentro de un atributo no se toca en absoluto.
Dónde no basta htmlspecialchars
htmlspecialchars() es el escapado adecuado para el texto HTML y los atributos entre comillas. Otros lugares de una página tienen otras reglas:
- En una URL,
http_build_query()ourlencode()codifican el valor; luegohtmlspecialchars()hace que el&entre parámetros sea HTML válido. - En JavaScript,
json_encode()produce un valor JS válido, yJSON_HEX_TAGconvierte<y>en\u003Cy\u003E, para que un</script>en los datos no pueda cerrar la etiqueta script. - Nunca imprimas entrada del usuario en un
hrefsin comprobar el esquema:htmlspecialchars('javascript:alert(1)')no cambia y sigue ejecutándose al hacer clic. Acepta solo URLshttpyhttps, como se muestra en la página de filter_var.
Para el manejo de formularios que reúne todo esto, consulta los formularios en PHP.
Preguntas frecuentes
¿Qué hace htmlspecialchars en PHP?
Sustituye &, <, >, " y ' por &, <, >, " y '. El navegador muestra entonces esos caracteres en lugar de leerlos como HTML, así que un <script> escrito en un formulario se muestra como texto y nunca se ejecuta.
¿Cuál es la diferencia entre htmlspecialchars y htmlentities?
htmlspecialchars() convierte solo los cinco caracteres especiales de HTML. htmlentities() convierte además todos los caracteres que tienen una entidad con nombre, así que café se convierte en café. Con páginas UTF-8 las dos son igual de seguras, y htmlspecialchars() mantiene legible la salida, así que es la opción habitual.
¿Sigo necesitando ENT_QUOTES en PHP 8?
No por seguridad: desde PHP 8.1 las opciones por defecto son ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, así que las comillas simples también se escapan. Muchos proyectos siguen pasando ENT_QUOTES, 'UTF-8' de forma explícita para que la llamada se comporte igual en versiones antiguas y resulte obvia a quien la lee.
¿Uso htmlspecialchars en la entrada o en la salida?
En la salida. Guarda y valida el valor sin modificar, y escápalo en el momento de imprimirlo en HTML. Escapar en la entrada guarda < en tu base de datos, rompe las longitudes y las búsquedas, y lleva a un doble escapado como &lt;.
¿Basta strip_tags para prevenir XSS?
No. strip_tags() elimina las etiquetas, pero su parámetro de etiquetas permitidas conserva sus atributos, así que <b onclick="..."> sobrevive, y no hace nada con el texto colocado dentro de un atributo. Usa htmlspecialchars() al imprimir la entrada del usuario.