Hinzufügen von siteübergreifendem Skriptschutz zu ASP.NET 1.0
Scott Hanselman
Chefarchitekt
Corillian Corporation
November 2003
Zusammenfassung: ASP.NET 1.1 wurde das ValidateRequest-Attribut hinzugefügt, um Ihre Website vor websiteübergreifendem Skripting zu schützen. Was tun Sie jedoch, wenn auf Ihrer Website noch ASP.NET 1.0 ausgeführt wird? Scott Hanselman zeigt, wie Sie Ihren ASP.NET 1.0-Websites ähnliche Funktionen hinzufügen können. (12 gedruckte Seiten)
Inhalte
Das Problem
C#-Eye für den IL Guy
HttpModule
Programmiererabsicht
Installation und Konfiguration
Die Ergebnisse
Zusammenfassung
Problemstellung
Ich habe einen Kunden, der eine Website auf Microsoft® ASP.NET und microsoft® .NET Framework 1.0 bereitgestellt hat. Es ist ein großer Standort, und sie sind ein großer Kunde, und als großer Kunde neigen sie dazu, sich zu bewegen, nun ja, langsam. Wir befanden uns mitten in einer großen Bereitstellung, als ASP.NET/Framework 1.1 herauskam. Das Team fand es zu riskant, alles auf ASP.NET/Framework 1,1 so nah am Ziel zu verschieben. Also haben wir uns entschieden, später im Jahr auf ASP.NET/Framework 1.1 umzusteigen. Da wir jedoch komplexe E-Banking-Websites erstellen, die über viele Branchen hinweg bestehen und mit dem Geld der Leute umgehen, ist Sicherheit Job Nr. 1 (oder Job #0, wenn Sie nullbasiert sind). Der Client hat die Anforderung, dass wir mit siteübergreifenden Skriptingangriffen (häufig als "XSS" bezeichnet) aggressiv umgehen.
XSS ist eine besonders finstere Art des Hackens, bei der ein l33t hx0r (Elite Hacker) oder ein "Script Kiddie" versucht, persönliche Informationen abzurufen oder eine Website zu täuschen, um etwas zu tun, das sie nicht tun sollte, indem Sie JavaScript in ein Webformular eingeben oder das Skript in einen Parameter in der URL codieren. Ein einfaches Beispiel ist ein Webformular, das über ein einzelnes Textfeld und eine einzelne Schaltfläche verfügt. Der Benutzer gibt seinen Namen in das Textfeld ein und übermittelt das Formular. Die Seite druckt dann "Hello firstname" ** nach Zeichenfolgenverkettung, String.Format, Response.Write oder über eine serverseitige Bezeichnung aus.
Abbildung 1. Eingeben von Text; scheint sicher genug zu sein
Da die Seite die Benutzereingaben nimmt und sie direkt "regurgitatiert", wenn ich ein Schimpfwort eingeben würde, würde ich eine andere Art von Begrüßung erhalten! Was passiert aber, wenn der Benutzer statt seines Namens ein Skriptfragment wie "<Skriptwarnung>('bad stuff happens') eingibt;</script>." Der Code Behind sieht wie folgt aus:
if (this.IsPostBack) Response.Write("Hello " + this.TextBox1.Text);
Sie können sehen, dass der Inhalt des Textfelds direkt in den Antwortdatenstrom geschrieben und das JavaScript im Browser des Benutzers ausgewertet wird! Dies ist ein triviales Beispiel, aber stellen Sie sich vor, dass das schädliche JavaScript Code enthält, um auf die Cookies-Sammlung des Benutzers zuzugreifen oder einen Formularbeitrag an eine andere Website umzuleiten?
Abbildung 2. Eingeben von JavaScript, wo Text erwartet wird
Abbildung 3. JavaScript wird bei Antwort ausgeführt.
Der Einfachheit halber möchten wir keine zusätzliche Komplexität in unsere Webebene oder Geschäftslogik integrieren, um damit zu umgehen, dass jemand JavaScript in ein Formularfeld oder eine andere Kikane eingibt. Wir möchten uns mit XSS auf eine zentrale Weise befassen, vielleicht als Filter, früher in der HTTP-Workeranforderungskette, sicherlich bevor die eigentliche Seite ausgeführt wird. Nun, ASP.NET 1.1 enthält eine neue @Page Direktive, um genau dies zu tun! Die Eingabeüberprüfung ist standardmäßig aktiviert und kann mit dem ValidateRequest-Attribut der @Page Direktive gesteuert werden.
<%@ Page language="c#" Codebehind="WebForm1.aspx.cs" ValidateRequest="true" AutoEventWireup="false" Inherits="Junk.WebForm1" %>
ASP.NET 1.1-Anforderungsüberprüfung fängt schädlichen Skriptcode in den Beiträgen Cookie Collection, QueryString und Forms ab. Alle Eingabedaten werden anhand einer Liste potenziell gefährlicher Werte überprüft. Wenn Sie befürchten, dass diese Art der Validierung die Funktionalität Für Ihre Benutzer in irgendeiner Weise beeinträchtigt, lassen Sie mich Ihnen versichern, dass ihre Benutzer, wenn Sie JavaScript in Ihre Formularfelder eingeben, nicht die gewünschte Art von Benutzern sind. ValdidateRequest=true beeinträchtigt ihre Benutzerfreundlichkeit in keiner Weise. Wenn in einigen Eingabedaten ein schädliches Skript erkannt wird, wird eine HttpRequestValidationException ausgelöst. Sie können diesen Fehler sicherlich in Global.asax abfangen und die Standardfehlerseite durch Ihre persönlichen Bedrohungen ersetzen, wenn Sie möchten.
Es ist großartig, dass ASP.NET 1.1 diesen leistungsstarken Filter kostenlos enthalten hat, aber es hilft mir und dem ausstehenden ASP.NET 1.0-Websitestart meines Kunden nicht. Wie kann ich mich mit ASP.NET 1.0 vor websiteübergreifendem Skripting schützen, während ich auf das Upgrade meines Clients warte? Wir haben einige Ideen wie das Schreiben einiger regulärer Ausdrücke und das Durchsuchen der HTTP-Header in Application_BeginRequest herumgetreten, aber keine unserer Ideen fühlte sich gut an. Ich habe mich auch daran erinnert, dass ich für ein E-Finance-Unternehmen arbeite, nicht für ein Unternehmen, das Komponenten herstellt, um Cross-Site Scripting-Angriffe zu verhindern. Ich muss nicht versuchen, das Rad neu zu erfinden.
Dann erkannte ich, dass ich die Lösung direkt vor meinem Gesicht hatte; ASP.NET 1.1 dieses Problem bereits gelöst hatte, musste ich das Problem nur rückwärts lösen. Also entschloss ich mich, die vorhandene Version 1.1 zurück zu ASP.NET 1.0 zu portieren.
C#-Eye für den IL Guy
Um zu erkunden, was in ASP.NET 1.1 vor sich geht, brauchte ich ein Tool, das etwas höher war als ILDASM.EXE, der .NET-Disassembler, der im .NET Framework SDK enthalten ist. Wäre ich eine klügere Person, vielleicht könnte ich System.Web mit nur ILDASM auseinander nehmen, aber das Lesen von IL ist nicht trivial und ich hatte einen Zeitplan. Ich fand dieses Werkzeug im Reflektor von Lutz Roeder. Reflektor ist ein Objektbrowser, der Ihnen eine hervorragende Strukturansicht aller Namespaces und Klassen bietet, die die Basisklassenbibliothek (BCL) bereitstellt.
Abbildung 4. Betrachten der CrossSiteScriptingValidation-Klasse in Reflector
Abbildung 5. Exportieren von Quellcode für die CrossSiteScriptingValidation-Klasse
Der Reflector glänzt jedoch in seiner Fähigkeit, .NET-Assemblys zu dekompilieren und die Ergebnisse darzustellen, nicht als IL, sondern als gleichwertiger C#- oder Microsoft® Visual Basic® .NET-Code. Natürlich geht dabei eine offensichtliche Genauigkeit verloren, z. B. lokale Variablennamen, aber das ist Leben (und Code).
Also bin ich in System.Web herumgelaufen, bis ich eine interne Klasse namens CrossSiteScriptingValidation gefunden habe. Klang vielversprechend. Hier werden die schwierigen Fragen beantwortet, z. B. IsDangerousString oder IsDangerousScriptString. Alle Methoden in CrossSiteScriptingValidation geben boolesche Werte zurück. True gilt für die meisten als gefährlich. Aber welche Zeichenfolgen werden ausgewertet, und wer ruft diese Hilfsprogrammklasse auf? Mir schien, dass die Antwort in HttpRequest liegen würde, da wir versuchen, alle Anforderungen zu überprüfen.
HttpRequest enthält Auflistungen für Formularvariablen , Cookies und queryString. Diese Objekte vom Typ NameValueCollection (Cookies sind eigentlich eine HttpCookieCollection, die einige triviale zusätzliche Dinge enthält). Wenn Ihre URL also lautet https://localhost/junk/test.aspx?id=3, enthält die QueryString-Auflistung einen Eintrag für die Namens-ID mit dem Wert 3. HttpRequest verfügt über eine öffentliche get-Eigenschaft für diese Sammlung. Wenn Sie also Request.QueryString codieren, greifen Sie auf diese Eigenschaft zu. Hier geschieht alles. Wenn auf die Auflistung für den Vornamen zugegriffen wird, wird sie über ValidateNameValueCollection auf gefährliche Zeichenfolgen überprüft. Wenn keine HttpRequestValidationException ausgelöst wird, wird die jetzt gültige QueryString zurückgegeben, und ein Flag wird festgelegt, um den Mehraufwand für die erneute Überprüfung der Auflistung zu vermeiden.
if (this._flags[1] != null) { this._flags[1] = 0; this.ValidateNameValueCollection(this._queryString, "Request.QueryString"); } return this._queryString;
Validierungscode wie dieser erfolgt über die HttpRequest-Auflistungen in ASP.NET 1.1. Da ich natürlich eine Lösung möchte, die auf ASP.NET 1.0 ausgeführt wird und ich das Verhalten der Forms-, QueryString- und Cookie-Auflistungen nicht außer Kraft setzen kann, muss ich innerhalb der Aufrufliste eine weitere Möglichkeit zum Überprüfen der Sammlungen finden.
HttpModule
Ein HttpModule schien die perfekte Wahl zu sein. Eine einfache benutzerdefinierte öffentliche Klasse, die IHttpModule implementiert. Die IHttpModule-Schnittstelle besteht nur aus zwei Methoden: Init() und Dispose().. Init() wird einmal von ASP.NET mit der HttpApplication als einzigem Parameter aufgerufen und ist meine Möglichkeit, alle Ereignishandler mit der Anwendung zu verbinden. Aus Leistungsgründen wollte ich sicherstellen, dass mein websiteübergreifender Skriptüberprüfungscode nur einmal ausgeführt und vor und unabhängig von der Seite und der zugehörigen Geschäftslogik ausgeführt wurde.
Die HttpApplication verfügt über die folgenden Ereignisse, die in der angezeigten Reihenfolge ausgelöst werden:
- Beginrequest
- Authenticaterequest
- Authorizerequest
- Resolverequestcache
- [An diesem Punkt wird ein Handler (eine Seite, die der Anforderungs-URL entspricht) erstellt.]
- Acquirerequeststate
- Prerequesthandlerexecute
- [Der Handler wird ausgeführt. In unserem Fall die Seite]
- Postrequesthandlerexecute
- Releaserequeststate
- [Antwortfilter, falls vorhanden, filtern die Ausgabe.]
- Updaterequestcache
- Endrequest
Es sieht so aus, als ob die Zeit zum Ausführen des Validierungssteuerelements während des PreRequestHandlerExecute-Ereignishandlers liegt, kurz vor der Seite selbst. Wenn ich etwas potenziell Gefährliches finde und eine Ausnahme auslöst, wird die Seite nie ausgeführt. Dies ist das gewünschte Verhalten.
Daher habe ich eine Klasse namens ValidateInput erstellt, die IHttpModule implementiert, und in Init() wird ein EventHandler für PreRequestHandlerExecute eingebunden, um meine benutzerdefinierte Funktion ValidateRequest aufzurufen. Es befindet sich in ValidateRequest , wo ich die Funktionen aufrufe, die ich aus ASP.NET 1.1 übertragen werde.
Außerdem füge ich eine schnelle Versionsüberprüfung hinzu, um sicherzustellen, dass niemand versucht, dieses Modul auf ASP.NET 1.1 zu verwenden. Ich hasse es, wenn jemand vergessen würde, dieses Modul zu entfernen, wenn wir auf 1.1 aktualisieren.
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) ; }
Ich habe PreRequestHandlerExecute mit der ValidateRequest-Methode meiner Klasse eingebunden. Da ich nicht in die Forms-, QueryString- und Cookies-Auflistungen eingebunden werden kann, muss ich die gesamte Anforderungsüberprüfung hier durchführen, um sicherzustellen, dass nur überprüfte Anforderungen an meinen Page-Handler übergeben werden.
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); }
In ValidateRequest habe ich meine eigenen Implementierungen von ValidateNameValueCollection und ValidateCookieCollection aufgerufen. Jede von ihnen durchläuft die bereits analysierten Sammlungen, die die Post-Daten des Formulars darstellen, einschließlich vorab analysierter Cookies und queryString.
Es ist wichtig zu wissen, dass die Analyse dieser HTTP-Headerdaten und das Organisieren in NameValueCollections sicher ist, da alle potenziell schädlichen Daten aus der Anforderung noch nicht den Page-Handler oder Browser erreicht haben. Wenn ich das Anwendungsereignis BeginRequest anstelle von PreRequestHandlerExecute ausgewählt hätte, hätte ich die unformatierte HTTP-Anforderung selbst analysieren müssen. Also, ich erhalte das Beste aus beiden Welten, mühsames Parsen wurde für mich durchgeführt (und ist bereits in gut getestetem Code) und die Seite wurde noch nicht ausgeführt, was mir Zeit gibt, möglicherweise eine Ausnahme auszulösen und die Ausführung der Anforderung zu beenden.
Als Nächstes habe ich alle anderen Hilfsfunktionen in meine neue Klasse gezogen, einschließlich IsDangerousExpressionString, IsDangerousOnString, IsDangerousScriptString, IsDangerousString und IsAtoZ von Reflector. Es ist erwähnenswert, dass der dekompilierte C#-Code, den Reflector anzeigt, tatsächlich eine neue C#-Darstellung der in der Assembly enthaltenen IL ist. Die Namen der lokalen Variablen wurden geändert, und was früher eine Schleife war, kann jetzt eine Reihe von goto- und if-Anweisungen sein. Beurteilen Sie den Schreiber des Codes nicht anhand der IL-Darstellung! Denken Sie daran, dass der Compiler beim Generieren der endgültigen IL Freiheiten nehmen muss, und was wichtiger ist, ist das Konzept der Programmiererabsicht. Ich werde etwas später weiter unten darüber sprechen.
Abbildung 6. Betrachten der IsAtoZ-Methode
Nun benötigen wir eine benutzerdefinierte Exception-Klasse , die von ApplicationException abgeleitet ist und den passenden Namen HttpRequestValidiationException trägt. Dies ist zufällig derselbe Name, den ASP.NET 1.1 verwendet, aber in einem anderen Namespace. Diese Ausnahme wird ausgelöst, wenn ein potenziell gefährlich aussehendes Skript in httpRequest angezeigt wird. Wenn Sie die Ausnahmeseite anzeigen oder die Ausnahme protokollieren möchten, liegt es an Ihnen. Einige sind möglicherweise der Meinung, dass ein potenzieller Skriptangriff ein bedeutendes Ereignis ist, und entscheiden sich möglicherweise dafür, diese Ausnahme anders zu behandeln. Achten Sie in beiden Richtungen darauf, dass Sie über eine Ausnahmebehandlungsstrategie verfügen.
Programmiererabsicht
Ich wollte etwas über die Absicht des Programmierers Erwähnung. Was hier wirklich dekompiliert wurde, ist die Absicht des Programmierers. Wir sehen uns den C#-Quellcode nicht so an, wie er vom ursprünglichen Writer geschrieben wurde. Beim Dekompilieren in IL und anschließender Konvertierung in eine C#-Darstellung derselben IL ändern sich die Dinge. Ein Wenig Code aus IsDangerousOnString sieht beispielsweise in Reflector wie folgt aus:
goto L_0045; L_0040: index = (index + 1); L_0045: if (index >= len) { goto L_005E; } if (CrossSiteScriptingValidation.IsAtoZ(s[index])) { goto L_0040; }
Dies ist für den durchschnittlichen Programmierer schwer zu lesen, aber es vermittelt die Absicht des Programmierers richtig. Aber was war diese Absicht? Wir können den Code nur bis jetzt "falten". Möglicherweise handelte es sich um einen Aufruf von String.IndexOf , der für alles, was wir wissen, in der Zeile war. Wir können es jedoch wie folgt umschreiben (oder ein halbes Dutzend andere Möglichkeiten), damit wir es besser verstehen können:
//Programmer intent: look for non-alphas... while (index < len) { if (!CrossSiteScriptingValidation.IsAtoZ(s[index])) break; index++; }
Denken Sie daran: "Gotos gilt als schädlich" gilt nur für SIE, nicht für den Compiler! Beachten Sie auch, dass dieser Code auch als "for"-Schleife oder ein anderes Schleifenkonstrukt ausgedrückt worden sein könnte und die Absicht weiterhin korrekt ausgedrückt wird.
Installation und Konfiguration
Um ValidateInputASPNET10 auf dem Webserver zu installieren, müssen wir es nur der Liste der httpModule hinzufügen, die in unserer web.config konfiguriert sind. Die Assembly, in diesem Fall ValidateInputASPNET10.dll muss sich im Ordner \bin unserer Website und allen anderen Websites in unserem Feld befinden, die wir schützen möchten.
<configuration> <system.web> <httpModules> <add name="ValidateInput" type="Corillian.Web.ValidateInput,ValidateInputASPNET10" /> </httpModules> </system.web> </configuration>
Das Ergebnis
Wenn ich httpModule dem web.config hinzu füge, kann ich die gleiche ASP.NET Anwendung ohne erneute Kompilierung starten, da es sich bei HttpModule um eine eigene Assembly und ein eigenes Microsoft® Visual Studio® .NET-Projekt handelt. Beim Start ruft ASP.NET Init() für das neue ValidateInputASPNET10-HttpModule auf und verkettet mit dem PreRequestHandlerExecute-Ereignis. Wenn ich versuche, JavaScript wie zuvor in das Formular (oder die QueryString - oder Cookies-Sammlung ) einzugeben, wird mir diese Fehlermeldung angezeigt, die eine HttpRequestValidationException deklariert. Beachten Sie, dass ein Teil von JavaScript angezeigt wird, aber nur ein Teil. Wir möchten nicht, dass die Fehlermeldung ausgegeben und dasselbe JavaScript ausgeführt wird, vor dem wir uns schützen möchten.
Abbildung 7. Schützen Ihrer Website vor Skripteingaben
Hinweis Denken Sie daran, dass die Dekompilierung in erster Linie für das Debuggen und Ihre persönliche Ausbildung verwendet werden sollte. Achten Sie darauf, die Regeln für geistiges Eigentum zu beachten, und denken Sie daran, dass nicht verborgene Assemblys einfacher zu dekompilieren sind als C++-Anwendungen, dies uns keine Freibriefe zum Wischen von Code gibt. Wenn Sie sich Sorgen um Ihren Code und ihren geistigen Wohlstand machen, sehen Sie sich die Dotfuscator Community Edition an, die im Lieferumfang von Visual Studio .NET 2003 enthalten ist.
Zusammenfassung
Cross-Site Scripting ist eine der vielen Arten von Hacks, über die Sie sich beim Erstellen von ASP.NET Websites Sorgen machen müssen. Hacker können diese Technik verwenden, um Code auf dem Server auszuführen, was möglicherweise zu Datenverlust oder schlimmerem Diebstahl von Kundeninformationen führt. Die defensive Programmierung erfordert, dass Sie sich vor diesen Angriffen schützen. Das Hinzufügen von Validierungen zu Eingaben, wie in diesem Artikel ausgeführt, ist ein erster Schritt zum Schutz Ihrer Website.
Zum Autor
Scott Hanselman ist Chefarchitekt bei der Corillian Corporation, einem E-Finance-Enabler. Er verfügt über mehr als ein Jahrzehnt Erfahrung in der Entwicklung von Software in C, C++, Visual Basic, COM und derzeit Visual Basic .NET und C#. Scott ist stolz darauf, dass er in den letzten drei Jahren zum MSDN-Regionaldirektor für Portland, Oregon ernannt wurde, inhalte für Entwicklertage und die Einführung von Visual Studio .NET in Portland und Seattle entwickelt und gesprochen hat. Scott sprach auch auf der Einführung von Microsoft® Windows Server™ 2003 und Visual Studio .NET 2003 in vier Städten. Er spricht international über Microsoft-Technologien und hat zwei Bücher von Wrox Press mitverfasst. Im Jahr 2001 sprach Scott auf einer 15-Städte-Tour mit Microsoft, Compaq und Intel über Microsoft-Technologien und die Evangelisierung bewährter Designpraktiken. In diesem Jahr sprach Scott auf der Windows Server 2003-Einführungsveranstaltung in 4 PacWest-Städten, bei TechEd in den USA und in Malaysia sowie bei ASPLive in Orlando. Seine Gedanken zum Zen von .NET, Programmierung und Webdiensten finden Sie unter http://www.computerzen.com.