Agregar protección de scripting entre sitios a ASP.NET 1.0
Scott Hanselman
Arquitecto jefe
Corillian Corporation
Noviembre de 2003
Resumen: ASP.NET 1.1 agregó el atributo ValidateRequest para proteger el sitio del scripting entre sitios. ¿Qué hace, sin embargo, si el sitio web sigue ejecutándose ASP.NET 1.0? Scott Hanselman muestra cómo puede agregar funcionalidad similar a los sitios web de ASP.NET 1.0. (12 páginas impresas)
Contenido
El problema
C#-Eye for the IL Guy
HttpModule
Intención del programador
Instalación y configuración
Resultados
Conclusión
El problema
Tengo un cliente que ha implementado un sitio en Microsoft® ASP.NET y Microsoft® .NET Framework 1.0. Es un sitio grande, y son un cliente grande, y como un cliente grande, tienden a moverse, bien, lento. Estábamos en medio de una implementación grande cuando ASP.NET/Framework 1.1 salió. El equipo sintió que era demasiado arriesgado mover todo a ASP.NET/Framework 1.1 tan cerca de la línea de meta. Así que decidimos pasar a ASP.NET/Framework 1.1 más adelante en el año. Sin embargo, dado que creamos sitios web de banca electrónica complejos que cruzan muchas líneas de negocio y tratan con el dinero de la gente, la seguridad es el trabajo n.º 1 (o el trabajo #0 si no tiene base cero). El cliente tiene un requisito que tratamos con ataques de scripting entre sitios (a menudo denominados "XSS") de forma agresiva.
XSS es un tipo particularmente sinister de piratería, donde un l33t hx0r (hacker de élite) o un "script kiddie" intenta recuperar información personal o engañar a un sitio para hacer algo que no debería hacer escribiendo JavaScript en un formulario web o codificando el script en un parámetro en la dirección URL. Un ejemplo sencillo es un formulario web que tiene un único cuadro de texto y un solo botón. El usuario escribe su nombre en el cuadro de texto y envía el formulario. A continuación, la página imprime "Hello firstname" ** por concatenación de cadenas, String.Format, Response.Write o a través de una etiqueta del lado servidor.
Figura 1. Escribir texto; parece lo suficientemente seguro
Puesto que la página toma la entrada de los usuarios y "regurgitates" directamente, si especificó una palabra de ropa, obtendría un tipo diferente de saludo! Pero lo que sucede si, en lugar de escribir su nombre, el usuario escribe un fragmento de script como "<alerta de script>("sucede cosas incorrectas").</script>". El código subyacente tiene este aspecto:
if (this.IsPostBack) Response.Write("Hello " + this.TextBox1.Text);
Puede ver que el contenido del cuadro de texto se escribirá directamente en el flujo de respuesta y que javaScript se evaluará en el explorador del usuario. Este es un ejemplo trivial, pero imagine si el código JavaScript malintencionado contenía para acceder a la colección de cookies del usuario o redirigir una publicación de formulario a otro sitio?
Ilustración 2. Especificación de JavaScript donde se espera texto
Figura 3. JavaScript se ejecuta en la respuesta
Por motivos de simplicidad, preferimos no crear complejidad adicional en el nivel web o la lógica de negocios para tratar con alguien que escribe JavaScript en un campo de formulario o en alguna otra chicanera. Nos gustaría tratar con XSS de alguna manera central, quizás como filtro, anteriormente en la cadena de solicitudes de trabajo HTTP, ciertamente antes de que se ejecute la página real. Bueno, ASP.NET 1.1 incluye una nueva @Page directiva para hacer esto. La validación de entrada está activada de forma predeterminada y se puede controlar con el atributo ValidateRequest de la @Page directiva.
<%@ Page language="c#" Codebehind="WebForm1.aspx.cs" ValidateRequest="true" AutoEventWireup="false" Inherits="Junk.WebForm1" %>
ASP.NET 1.1 la validación de solicitudes detecta código de scripting malintencionado en la colección de cookies, queryString y entradas de formularios. Comprueba todos los datos de entrada en una lista de valores potencialmente peligrosos. En caso de que esté preocupado por que este tipo de validación afecte a la funcionalidad de los usuarios de alguna manera, déjame asegurarte de que si los usuarios escriben JavaScript en los campos de formulario, no son el tipo de usuarios que quieres. ValdidateRequest=true no impedirá la experiencia de los usuarios de ninguna manera. Si se detecta un script malintencionado en algunos datos de entrada, se produce una excepción HttpRequestValidationException . Sin duda, puede detectar este error en Global.asax y reemplazar la página de error predeterminada por sus propias amenazas personales si lo desea.
Es genial que ASP.NET 1.1 ha incluido este potente filtro de forma gratuita, pero no me ayuda y el lanzamiento del sitio 1.0 de mi cliente está ASP.NET pendiente. ¿Cómo puedo protegerse frente al scripting entre sitios con ASP.NET 1.0 mientras espero a que mi cliente se actualice? Hemos iniciado algunas ideas como escribir algunas expresiones regulares y buscar los encabezados HTTP en Application_BeginRequest, pero ninguna de nuestras ideas se sintió bien. También me recordé que trabajo para una empresa de finanzas electrónicas, no para una empresa que hace componentes para evitar ataques de scripting entre sitios. No necesito que intente reinventar la rueda.
Entonces me di cuenta de que tenía la solución sentada justo delante de mi cara; ASP.NET 1.1 ya había resuelto este problema, sólo necesitaba resolver el problema hacia atrás. Por lo tanto, decidí volver a portar la versión 1.1 existente a ASP.NET 1.0
C#-Eye for the IL Guy
Para explorar lo que estaba ocurriendo dentro de ASP.NET 1.1, necesitaba una herramienta que era un poco más alto que ILDASM.EXE, el desensamblador de .NET incluido con el SDK de .NET Framework. ¿Era una persona más inteligente, tal vez podría apartar System.Web con sólo ILDASM, pero leer IL es no trivial y tuve una programación. Encontré esa herramienta en el Reflector de Lutz Roeder. Reflector es un explorador de objetos que proporciona una excelente vista de árbol de todos los espacios de nombres y clases que proporciona la biblioteca de clases base (BCL).
Figura 4. Examinar la clase CrossSiteScriptingValidation en Reflector
Figura 5. Exportación del código fuente para la clase CrossSiteScriptingValidation
Sin embargo, donde Reflector realmente brilla es en su capacidad de descompilar ensamblados .NET y presentar los resultados, no como IL, sino código de C# o Microsoft® Visual Basic® .NET equivalente. Por supuesto, se pierde cierta fidelidad obvia en el proceso, como nombres de variables locales, pero esa es la vida (y el código).
Por lo tanto, corrí en System.Web hasta que encontré una clase interna llamada CrossSiteScriptingValidation. Sonaba prometedor. Aquí es donde se responden las preguntas difíciles, como IsDangerousString o IsDangerousScriptString. Todos los métodos de CrossSiteScriptingValidation devuelven valores booleanos; true en la mayoría califica como peligroso. ¿Pero qué cadenas estamos evaluando y quién llama a esta clase de utilidad? Me parecía que la respuesta estaría en HttpRequest mientras intentamos validar todas las solicitudes.
HttpRequest contiene colecciones para variables form , Cookies y QueryString. Estos objetos de tipo NameValueCollection (las cookies son realmente httpCookieCollection, que tiene algo adicional trivial), por lo que si la dirección URL es https://localhost/junk/test.aspx?id=3, la colección QueryString contendrá una entrada para el identificador de nombre con el valor 3. HttpRequest tiene una propiedad get pública para esta colección, por lo que cuando se codifica Request.QueryString, se accede a esa propiedad. Aquí es donde todo sucede. Cuando se accede a la colección para el nombre, se comprueba si hay cadenas peligrosas a través de ValidateNameValueCollection. Si no se produce una excepción HttpRequestValidationException , se devuelve el elemento QueryString válido y se establece una marca para evitar la sobrecarga de volver a comprobar la colección.
if (this._flags[1] != null) { this._flags[1] = 0; this.ValidateNameValueCollection(this._queryString, "Request.QueryString"); } return this._queryString;
El código de validación como este es todo a través de las colecciones HttpRequest en ASP.NET 1.1. Por supuesto, como quiero una solución que se ejecute en ASP.NET 1.0 y no puedo invalidar el comportamiento de las colecciones Forms, QueryString y Cookie , tendré que encontrar otra oportunidad dentro de la pila de llamadas para validar las colecciones.
HttpModule
Un HttpModule parecía la elección perfecta. Una clase pública personalizada sencilla que implementa IHttpModule. La interfaz IHttpModule consta de solo dos métodos, Init() y Dispose().. Se llama a Init() una vez por ASP.NET con HttpApplication como único parámetro, y es mi oportunidad de enlazar cualquier controlador de eventos a la aplicación. Por motivos de rendimiento, quería asegurarme de que mi código de validación de scripting entre sitios solo se ejecutó una vez y ejecutó antes e independientemente de la página y la lógica de negocios asociada.
HttpApplication tiene estos eventos que se activan en el orden que se muestra:
- BeginRequest
- AuthenticateRequest
- AuthorizeRequest
- ResolveRequestCache
- [Un controlador (una página correspondiente a la dirección URL de solicitud) se crea en este momento.
- AcquireRequestState
- PreRequestHandlerExecute
- [Se ejecuta el controlador. En nuestro caso, la página]
- PostRequestHandlerExecute
- ReleaseRequestState
- [Filtros de respuesta, si existe, filtra la salida.]
- UpdateRequestCache
- EndRequest
Parece que el tiempo para ejecutar el validador es durante el controlador de eventos PreRequestHandlerExecute , justo antes de la propia página. Si encuentro algo potencialmente peligroso y produce una excepción, la página nunca se ejecutará. Este es el comportamiento deseado.
Por lo tanto, he creado una clase llamada ValidateInput que implementa IHttpModule y en init() enlaza un EventHandler para PreRequestHandlerExecute para llamar a mi función personalizada, ValidateRequest. Estará dentro de ValidateRequest , donde llamaré a las funciones que traeré de ASP.NET 1.1.
También agregaré una comprobación de versión rápida para asegurarse de que nadie intente usar este módulo en ASP.NET 1.1. Odio que alguien olvide quitar este módulo cuando actualicemos a la versión 1.1.
public class ValidateInput : IHttpModule { HttpContext context; HttpApplication application; public ValidateInput(){} public void Init(HttpApplication app) { Version v = System.Environment.Version; if (v.Major != 1 && v.Minor != 0) throw new NotSupportedException(@"The ValidateInput HttpModule is not supported on this version of ASP.NET. Remove it from your Web.config file!"); app.PreRequestHandlerExecute += new EventHandler(this.ValidateRequest) ; }
He conectado PreRequestHandlerExecute al método ValidateRequest de mi clase. Puesto que no puedo enlazar con las colecciones Forms, QueryString y Cookies , deberá realizar toda la validación de solicitudes aquí para asegurarse de que solo se pasan solicitudes validadas a mi controlador de páginas .
public void ValidateRequest(Object src, EventArgs e) { //Store away what may be useful during this Request... application = (HttpApplication)src; context = application.Context; this.ValidateNameValueCollection(context.Request.Form, "Request.Form"); this.ValidateNameValueCollection(context.Request.QueryString, "Request.QueryString"); this.ValidateCookieCollection(context.Request.Cookies); }
En ValidateRequest , he llamado a mis propias implementaciones de ValidateNameValueCollection y ValidateCookieCollection. Cada una de ellas recorre las colecciones ya analizadas que representan los datos POST del formulario, incluidas las cookies previamente analizadas y QueryString.
Es importante saber que el análisis de estos datos de encabezado HTTP y la organización en NameValueCollections es seguro, ya que los datos potencialmente malintencionados de la solicitud aún no han llegado al controlador de páginas o al explorador. Además, si hubiera elegido el evento de aplicación BeginRequest en lugar de PreRequestHandlerExecute, habría tenido que analizar la solicitud HTTP sin procesar yo mismo. Por lo tanto, obtengo lo mejor de ambos mundos, el análisis tedioso se ha hecho para mí (y ya está en código bien probado) y la página aún no se ha ejecutado, dándome tiempo para posiblemente producir una excepción y detener la ejecución de la solicitud.
A continuación, extraí todas las demás funciones auxiliares en mi nueva clase, incluyendo IsDangerousExpressionString, IsDangerousOnString, IsDangerousScriptString, IsDangerousString e IsAtoZ del reflector. Merece la pena mencionar que el código de C# descompilado que reflector muestra es realmente una nueva representación de C# del IL contenido en el ensamblado. Los nombres de variables locales se han cambiado y lo que fue una vez un bucle ahora puede ser una serie de instrucciones goto y if. ¡No juzgue el escritor del código de la representación il! Recuerde que el compilador debe tomar libertades al generar la IL final y lo que es más importante es el concepto de intención del programador. Hablaré de esto un poco más adelante.
Figura 6. Examinar el método IsAtoZ
Ahora, necesitaremos una clase Exception personalizada que derive de ApplicationException que se denominará httpRequestValidiationException. Esta coincidencia es el mismo nombre que usa ASP.NET 1.1, pero en un espacio de nombres diferente. Esta excepción se producirá si aparece un script potencialmente peligroso en HttpRequest. Si decide mostrar la página de excepciones o registrar la excepción, es su caso. Algunos pueden sentir que un posible ataque de script es un evento significativo y puede optar por controlar esta excepción de forma diferente. En cualquier caso, asegúrese de tener una estrategia de control de excepciones en vigor.
Intención del programador
Quería mencionar un poco sobre la intención del programador. Lo que realmente se ha descompilado aquí es la intención del programador. En realidad, no estamos examinando el código fuente de C# como el escritor original lo escribió. Al descompilar en IL, después convertir en una representación de C# de ese mismo IL, cambian las cosas. Por ejemplo, un poco de código de IsDangerousOnString tiene este aspecto en Reflector:
goto L_0045; L_0040: index = (index + 1); L_0045: if (index >= len) { goto L_005E; } if (CrossSiteScriptingValidation.IsAtoZ(s[index])) { goto L_0040; }
Esto es difícil de leer para el programador promedio, pero transmite correctamente la intención del programador. ¿Pero qué era esa intención? Podemos "plegar" el código de copia de seguridad solo hasta ahora. Podría haber sido una llamada a String.IndexOf que estaba en línea para todo lo que sabemos. Sin embargo, podemos reescribirlo como este (o media docena de otras maneras) para que podamos entenderlo mejor:
//Programmer intent: look for non-alphas... while (index < len) { if (!CrossSiteScriptingValidation.IsAtoZ(s[index])) break; index++; }
Recuerde que "Gotos se considera perjudicial" solo se aplica a YOU, no al compilador. Tenga en cuenta también que este código también se podría haber expresado como un bucle "for" o alguna otra construcción de bucle, y la intención todavía se expresa correctamente.
Instalación y configuración
Para instalar ValidateInputASPNET10 en el servidor web, solo es necesario agregarlo a la lista de httpModules configurados en nuestra web.config. En este caso, el ensamblado ValidateInputASPNET10.dll debe residir en la carpeta \bin de nuestro sitio y en cualquier otro sitio de nuestro cuadro que deseemos proteger.
<configuration> <system.web> <httpModules> <add name="ValidateInput" type="Corillian.Web.ValidateInput,ValidateInputASPNET10" /> </httpModules> </system.web> </configuration>
Resultado
Cuando agrego httpModule a la web.config, podré iniciar la misma aplicación de ASP.NET sin volver a compilar, ya que HttpModule es su propio ensamblado y el proyecto .NET de Microsoft® Visual Studio®. Al iniciarse, ASP.NET llamará a Init() en el nuevo ValidateInputASPNET10 HttpModule y se encadenará al evento PreRequestHandlerExecute . Si intento escribir JavaScript en el formulario (o queryString o colección de cookies ) como antes, se me presenta este mensaje de error declarando una httpRequestValidationException. Observe que parte de JavaScript se muestra, pero solo parte; no queremos que el mensaje de error se produzca y ejecute el mismo JavaScript del que estamos intentando protegernos.
Ilustración 7. Protección del sitio web frente a la entrada de script
Nota Recuerde que la descompilación debe usarse principalmente para la depuración y su educación personal. Asegúrese de tener en cuenta las reglas de propiedad intelectual y recuerde que solo porque los ensamblados sin usar son más fáciles de descompilar que las aplicaciones de C++, esto no nos da un blanco a la carta para deslizar el código. Si le preocupa el código y la prosperidad intelectual, eche un vistazo a dotfuscator Community Edition que se incluye con Visual Studio .NET 2003.
Conclusión
El scripting entre sitios es uno de los muchos tipos de hacks que debe preocuparse al crear ASP.NET sitios web. Los hackers pueden usar esta técnica para ejecutar código en el servidor, lo que puede dar lugar a la pérdida de datos, o peor, robo de información del cliente. La programación defensiva exige protegerse de estos ataques. Agregar validación a la entrada, como se ha hecho en este artículo, es un primer paso para proteger el sitio web.
Acerca del autor
Scott Hanselman es arquitecto jefe de corillian Corporation, un habilitador de finanzas electrónicas. Tiene más de una década de experiencia en el desarrollo de software en C, C++, Visual Basic, COM y actualmente en Visual Basic .NET y C#. Scott está orgulloso de haber sido nombrado director regional de MSDN para Portland, Oregon durante los últimos tres años, desarrollando contenido para y hablando en Developer Days y el lanzamiento de Visual Studio .NET en Portland y Seattle. Scott también habló en los lanzamientos de Microsoft® Windows Server™ 2003 y Visual Studio .NET 2003 en 4 ciudades. Ha hablado internacionalmente en tecnologías de Microsoft y ha creado dos libros de Wrox Press. En 2001, Scott habló en una gira nacional de 15 ciudades con Microsoft, Compaq e Intel con tecnologías de Microsoft y la evangelización de buenas prácticas de diseño. Este año Scott habló en el evento de lanzamiento de Windows Server 2003 en 4 ciudades de PacWest, en TechEd en Estados Unidos y en Malasia, y en ASPLive en Orlando. Sus pensamientos sobre el Zen de .NET, Programación y servicios web se pueden encontrar en http://www.computerzen.com.