XSS y las peculiaridades de los navegadores

Hay ocasiones en que la forma como interpreta HTML un navegador puede producir algunos problemas de seguridad, esto normalmente se debe a descuidos de un programador que asume cierta funcionalidad.

Veamos el siguiente ejemplo — basado en una aplicación del mundo real ™ — que muestra un caso de estos:

php:
<?php

if (empty($_GET['el'])) {
        die;
}
/* Función genérica que sirve para eliminar ciertos caracteres */
function clean_input_string($string) {
        return preg_replace('/[ <>\'"\r\n\t\\()]/', '', stripslashes($string));
}
$elemento = clean_input_string($_GET['el']);

?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
    <title>Demo</title>
        <script type="text/javascript">
            //<![CDATA[
            function ponerFoco(id){
                        var elemento = document.getElementById(id);
                        if (!elemento) return;
                        elemento.focus();
                }
            //]]>
        </script>
</head>

<body onload="ponerFoco('<?php echo $elemento; ?>')">   
        <form method="post" action="foo.php">
            <input type="text" name="usuario" id="usuario" />
                <input type="text" name="contrasena" id="contrasena" />
               
                <input type="submit" name="postback" value="Entrar »" />
        </form>
</body>

</html>

Como se puede observar, el código lo único que hace es intentar poner el foco en el elemento que se especifique en el parámetro el, éste valor antes es filtrado (elimina los caracteres espacio, <, >, ', ", \r, \n, \t, (, ) y \ ) por la función de propósito general clean_input_string — la aplicación de donde se tomó el código hace uso de esa función en varias partes.

Puesto que se eliminan los caracteres \, (, ) y ', en circunstancias normales no debería ser posible ejecutar javascript en el ejemplo mostrado; si el contiene ');alert(document.cookie)// entonces lo que llega al navegador es <body onload="ponerFoco(';alertdocument.cookie//')">, valor completamente inofensivo para nuestros propósitos.

Haciendo unas pruebas con un valor parecido al anterior, pero esta vez usando entidades HTML en lugar de los caracteres que son eliminados por la función clean_input_string, se consiguen resultados interesantes. Por ejemplo, para el = &#39;&#41;;alert&#40;document.cookie&#41;//, el HTML generado es:

html:
<body onload="ponerFoco('');alert(document.cookie)//')">

A simple vista, parece igual de inofensivo que el anterior caso, sin embargo si esa página carga en Firefox o Internet Explorer (no probé con otros navegadores), además de ejecutarse la función ponerFoco, se mostrará un mensaje mostrando las cookies almacenadas.

Este tipo de problemas se pueden solucionar evitando en lo posible enviar directamente los valores que dependen del cliente, definiendo filtros más específicos (formatos de identificadores válidos) o separando la generación de javascript y HTML en documentos distintos.