Referencias a caracteres y entidades de carácter

En el glosario XHTML+CSS se explican las referencias a caracteres. Hacemos un resumen para refrescar los conceptos. En la siguiente tabla puede ver los cinco caracteres reservados que sirven para codificar HTML:

carácterReferencia decimalReferencia hexadecimalNombre simbólico (entidad de carácter)
Menor que (less than sign)&#60; <&#x3C; <&lt; <
Mayor que (greater than sign)&#62; >&#x3E; >&gt; >
ampersand&#38; &&#x26; &&amp; &
Comilla doble (quotation mark)&#34; "&#x22; "&quot; "
Comilla simple (apostrophe mark)&#39; '&#x27; '&apos; '
Internet Explorer 8 no representa &apos; y lo escribe literalmente.

Puede verlos en el DTD-SPECIAL donde se incluyen las entidades de caracteres especiales para HTML. Una referencia a carácter es un forma de acceder al carácter UNICODE mediante una expresión que se inicia con el signo ampersand &. Con &#n; representamos mediante una referencia al carácter enésimo de la lista UNICODE. Si ese número se expresa en hexadecimal pondríamos &#xh;. Por último tenemos los nombres simbólicos o entidades de carácter. Se trata de dar un nombre para recordar mejor que el uso de los números UNICODE.

Es importante no confundir los términos:

  • Todos los caracteres UNICODE se pueden representar con una referencia a ese carácter.
  • Algunos caracteres UNICODE tienen un nombre simbólico y a veces se le dice que es una entidad de carácter. Si observa el DTD-SPECIAL que señalamos antes, lo de entidad se debe a que se definen como ENTITY dentro del DTD. Por ejemplo, con <!ENTITY quot "&#34;"> se define la entidad para la comilla doble dándole el nombre quot y la referencia UNICODE &#34;. De esta forma el navegador cuando llegue a un &quot; deberá traducirlo por &#34;. Por lo tanto las entidades de carácter no son otra cosa que referencias a caracteres.

Funciones PHP para filtrar tags de HTML

En el manual de PHP encontramos dos funciones muy importantes para el manejo del filtrado de caracteres:

  • htmlspecialchars($string[,$quote_style[,$charset[,$double_encode]]]): Se trata de sustituir los caracteres siguientes por sus referencias a caracteres (entidades de carácter):
    • & signo ampersand por &amp;
    • " comillas dobles por &quot; si el argumento $quote_style NO es ENT_NOQUOTES. Este argumento opcional tiene el valor por defecto ENT_COMPAT que sólo traduce las comillas dobles y deja intactas las simples.
    • ' comillas simples por &#039; únicamente cuando el argumento $quote_style es ENT_QUOTES
    • < signo menor que por &lt;
    • > signo mayor que por &gt;
  • htmlentities($string[,$quote_style[,$charset[,$double_encode]]]): En este caso además de los caracteres mencionados, todos los que tengan una entidad de carácter (un nombre simbólico) son traducidos.

Para entender mejor todo esto haremos una tabla con ejemplos:

funciónargumentosliteral HTML del resultado
$string$quote_style$charset$double-encode
Es lo mismo pasar la función sin argumentos o con los argumentos por defecto. Vemos que ENT_COMPAT traduce las comillas dobles como &quot; y deja intacta las simples. Al traducir los signos mayor y menor así como el ampersand, hacemos que ese resultado no sea un literal HTML y por lo tanto no pueda ser ejecutado.
htmlspecialchars<'&">&lt;'&amp;&quot;&gt;
htmlspecialchars<'&">ENT_COMPATUTF-8true&lt;'&amp;&quot;&gt;
Con ENT_QUOTES ambas comillas simples y dobles son traducidas a &#039; y &quot; respectivamente. Vea como la comilla simple o apóstrofe no se traduce por la entidad de carácter sino por su referencia directa (debido a que IE no la reconoce). Con ENT_NOQUOTES deja intactos ambos tipos de comillas.
htmlspecialchars<'&">ENT_QUOTES&lt;&#039;&amp;&quot;&gt;
htmlspecialchars<'&">ENT_NOQUOTES&lt;'&amp;"&gt;
Cuando pasamos una entidad de carácter como la de la flecha derecha ⇒ pasando el literal &rArr;, tenemos que el argumento $double_encoded por defecto es true, con lo que se traducen simpre, mientras que con false no se traduce. Observe como otros caracteres no ASCII como ñÇ que no se han expresado con entidades de carácter (&ntilde;&Ccedil;) se dejan intactos.
htmlspecialchars&rArr;ñÇENT_COMPATUTF-8true&amp;rArr;ñÇ
htmlspecialchars&rArr;ñÇENT_COMPATUTF-8false&rArr;ñÇ
La función htmlentities() es igual que htmlspecialchars() con la excepción de que traduce todos los caracteres pasados tal cual (no como literales de referencias), pero que tengan entidad de carácter. Por ejemplo, la ñ tiene la referencia &ntilde; mientras que la Ç tiene &Ccedil;. El argumento $double_encoded funciona igual para las referencias literales: a true (valor por defecto) se traducen mientras que con false no.
htmlentities<'&">&lt;'&amp;&quot;&gt;
htmlentities<'&">ENT_COMPATUTF-8true&lt;'&amp;&quot;&gt;
htmlentities<'&">ENT_QUOTES&lt;&#039;&amp;&quot;&gt;
htmlentities<'&">ENT_NOQUOTES&lt;'&amp;"&gt;
htmlentities&rArr;ñÇENT_COMPATUTF-8true&amp;rArr;&ntilde;&Ccedil;
htmlentities&rArr;ñÇENT_COMPATUTF-8false&rArr;&ntilde;&Ccedil;

También existen otras funciones como strip_tags($str[, $allowable_tags]) que eliminan los tags o etiquetas de HTML dejando el texto interior. Sin embargo no comprueba que el HTML sea conforme, es decir, esté validado y tenga todos los tags de apertura y cierre, lo que puede dar lugar a texto no esperado. Por otro lado el segundo argumento permite no eliminar los tags que se indiquen. Por ejemplo, con strip_tags($str, "<p>") extraería todo el texto de todos los elementos a excepción de los <p> que los deja intactos.

Filtrando con htmlspecialchars() y ENT_QUOTES

Si se usa htmlspecialchars() hay que tener en cuenta de pasar el argumento $quote_style con el valor ENT_QUOTES para que traduzca las comillas simples y dobles. En el tema anterior vimos un ejemplo de inyección en un atributo de un elemento <input>. El caso es que el literal de salida era también un cuadro de texto, que después de hacer los cambios para PHP incluyendo el valor recibido del formulario, quedaría así:

<input type='text'
value='<?php echo $_GET["campo"]; ?>' />

Hay que tener en cuenta que ahora no concatenamos cadenas sino que se trata del texto del propio documento PHP con HTML. Con echo incrustamos el valor recibido. La vulnerabilidad consistía en pasar en el cuadro de texto del formulario el valor ' onclick='alert("hola"). La primera comilla cerraba el value del cuadro de texto y la comilla final de este era la que cerraba el script del onclick. Si filtraramos la entrada con

htmlspecialchars("' onclick='alert(\"hola\")") 

tendríamos un resultado con el literal ' onclick='alert(&quot;hola&quot;) donde se han traducido únicamente las dobles comillas del interior de los paréntesis, pues el argumento $quote_style es ENT_COMPAT por defecto que sólo traduce las comillas dobles. Si luego le damos salida a este string incorporándolo en nuestro literal HTML queda como sigue:

"<input type='text'
value='' onclick='alert(&quot;hola&quot;)' />"

consiguiéndose introducir un atributo no deseado dentro del cuadro de texto de salida.

Pero en este ejemplo usamos comillas simples para el código del documento PHP con HTML, cuando lo normal es usarlas dobles:

<input type="text"
value="<?php echo $_GET["campo"]; ?>" />

quedando después de la inyección

<input type="text"
value="' onclick='alert(&quot;hola&quot;)" />

de tal forma que no se produce la citada inyección pues no hay comillas dobles en el interior del value puesto que han sido traducidas por htmlspecialchars(). Aunque de esta forma parece no afectar, es evidente del riesgo que se asume en el sentido de que HTML permite también usar comillas simples en los atributos. Por eso considero importante usar ENT_QUOTES para que traduzca también las comillas simples.

Filtrando con htmlentities()

A efectos de filtrar los caracteres reservados de HTML es indiferente usar htmlspecialchars() o htmlentities(). Esta última tiene la característica de traducir todos los caracteres a entidades que lo tengan. Pero esto no siempre es necesario a efectos de seguridad y puede hacerse buscando otros objetivos. Si se busca información sobre el tema podemos acabar un poco desorientados, pues el manual PHP tampoco da mucha más información al respecto.

Es evidente que traducir todas las entidades de caracteres cuando almacenamos las entradas en, por ejemplo, una base de datos, puede resultar en un desaprovechamiento de espacio. Los caracteres ASCII no extendidos, del 0 al 127, necesitan 1 byte en UTF-8. El resto ocuparán más de un byte. Así para la letra griega Ω necesitamos 2 bytes en UTF-8 (CE A9 en hexadecimal) mientras que su entidad &Omega; ocupará 6 bytes. O la letra Ñ que ocupa 2 bytes en UTF-8 (C3 91 en hexadecimal) mientras que &Ntilde; ocupa 7 bytes.

Puede ver el artículo algoritmos de transformación UTF-8 que explica como funciona esa codificación. Ahí se puede descargar un ejecutable hta para probar la codificación UTF-8 y obtener los bytes.

Para evitar el exceso de caracteres podría filtrarse a la salida de la base de datos, pero asumiendo el riesgo de que podemos tener registros no filtrados que en algún caso podrían escaparse de los filtros a la salida. Por lo tanto mi opinión es usar htmlspecialchars() con ENT_QUOTES siempre a la entrada. Y usar htmlentities() con el mismo argumento cuando queramos también traducir las entidades, en este caso a la salida de la base de datos.

Una razón para usar htmlentities() es universalizar los caracteres que no son comunes en todos las lenguas. Pero eso podía ser necesario hace unos años cuando no todos los sistemas soportaban UNICODE al completo. Hoy me parece imposible que algún sistema (navegador, etc.) no sea capaz de soportar algún carácter de UTF-8, por ejemplo. Si se codifica un documento HTML bien formado con

<meta http-equiv="content-type" content="text/html; charset=UTF-8" /> 

se supone que todo el documento tiene caracteres codificados en UTF-8. Si se envía por ejemplo una Ñ en un formulario, ¿qué necesidad hay de traducirla por &Ntilde;?, ¿es qué acaso un Noruego no va a ver la Ñ sin usar &Ntilde;?. El verá esa Ñ como puedo ver yo su letra Å sin necesidad de usar la entidad &Aring; siempre que los navegadores de ambos gestionen de la misma forma la codificación declarada en el documento.