Verhindern von Cross-Site Scripting (XSS) in ASP.NET Core

Von Rick Anderson

Cross-Site Scripting (XSS) ist ein Sicherheitsrisiko, das es Cyber-Angreifer:innen ermöglicht, clientseitige Skripts (in der Regel JavaScript) auf Webseiten zu platzieren. Wenn andere Benutzende betroffene Seiten laden, werden die Skripts der Cyber-Angreifer:innen ausgeführt. So können die Cyber-Angreifenden Cookies und Sitzungstoken stehlen, den Inhalt der Webseite durch DOM-Manipulation ändern oder den Browser auf eine andere Seite umleiten. XSS-Sicherheitsrisiken treten im Allgemeinen auf, wenn eine Anwendung Benutzereingaben übernimmt und auf einer Seite ausgibt, ohne sie zu überprüfen, zu codieren oder mit Escapezeichen zu versehen.

Dieser Artikel gilt in erster Linie für ASP.NET Core MVC mit Ansichten, Razor Pages und anderen Apps, die HTML zurückgeben, das für XSS anfällig sein könnte. Web-APIs, die Daten in Form von HTML, XML oder JSON zurückgeben, können XSS-Angriffe in ihren Client-Apps auslösen, wenn sie die Eingaben von Benutzenden nicht ordnungsgemäß bereinigen, je nachdem, wie viel Vertrauen die Client-App in die API setzt. Wenn z. B. eine API vom Benutzenden generierte Inhalte akzeptiert und diese in einer HTML-Antwort zurückgibt, könnte eine Cyber-Angreifer:in böswillige Skripts in den Inhalt einfügen, die ausgeführt werden, wenn die Antwort im Browser des Benutzenden gerendert wird.

Um XSS-Angriffe zu verhindern, sollten Web-APIs eine Eingabeüberprüfung und eine Codierung der Ausgabe implementieren. Die Eingabeüberprüfung stellt sicher, dass die Eingaben des Benutzers die erwarteten Kriterien erfüllen und keinen böswilligen Code enthalten. Die Codierung der Ausgabe stellt sicher, dass alle von der API zurückgegebenen Daten ordnungsgemäß bereinigt werden, sodass sie nicht als Code vom Browser des Benutzers ausgeführt werden können. Weitere Informationen finden Sie in diesem GitHub-Issue.

Schützen Ihrer Anwendung vor XSS

Grundsätzlich funktioniert XSS, indem Ihre Anwendung dazu gebracht wird, ein <script>-Tag in Ihre gerenderte Seite oder ein On*-Ereignis in ein Element einzufügen. Entwickler sollten die folgenden Präventionsschritte verwenden, um die Einführung von XSS in ihre Anwendungen zu vermeiden:

  1. Fügen Sie niemals nicht vertrauenswürdige Daten in Ihre HTML-Eingabe ein, es sei denn, Sie befolgen den rest der folgenden Schritte. Nicht vertrauenswürdige Daten sind alle Daten, die von Cyber-Angreifer:innen gesteuert werden können, z. B. HTML-Formulareingaben, Abfragezeichenfolgen, HTTP-Header oder sogar Daten, die aus einer Datenbank stammen, da ein Angreifer möglicherweise in der Lage ist, breach Ihre Datenbank zu kompromittieren, auch wenn sie Ihre Anwendung nicht breach können.

  2. Bevor Sie nicht vertrauenswürdige Daten in ein HTML-Element einfügen, vergewissern Sie sich, dass es HTML-codiert ist. Bei der HTML-Codierung werden Zeichen wie < in eine sichere Form wie < umgewandelt

  3. Bevor Sie nicht vertrauenswürdige Daten in ein HTML-Attribut eingeben, vergewissern Sie sich, dass es HTML-codiert ist. Die HTML-Attributcodierung ist eine Obermenge der HTML-Codierung und codiert zusätzliche Zeichen wie " und ".

  4. Bevor Sie nicht vertrauenswürdige Daten in JavaScript einfügen, platzieren Sie die Daten in einem HTML-Element, dessen Inhalt Sie zur Laufzeit abrufen. Wenn dies nicht möglich ist, stellen Sie sicher, dass die Daten in JavaScript codiert sind. Bei der JavaScript-Codierung werden für JavaScript gefährliche Zeichen durch ihren Hexadezimalwert ersetzt, d. h. < würde z. B. als \u003C codiert werden.

  5. Bevor Sie nicht vertrauenswürdige Daten in eine Zeichenfolge für eine URL-Abfrage eingeben, vergewissern Sie sich, dass sie URL-codiert ist.

HTML-Codierung mithilfe von Razor

Die in MVC verwendete Razor-Engine codiert automatisch alle Ausgaben, die von Variablen stammen, es sei denn, Sie arbeiten sehr hart daran, dies zu verhindern. Sie verwendet die Regeln für die HTML-Attributcodierung, wenn Sie die @-Direktive verwenden. Da die HTML-Attributcodierung eine Obermenge der HTML-Codierung ist, müssen Sie sich nicht darum kümmern, ob Sie die HTML-Codierung oder die HTML-Attributcodierung verwenden sollten. Sie müssen sicherstellen, dass Sie @ nur in einem HTML-Kontext verwenden und nicht, wenn Sie versuchen, nicht vertrauenswürdige Eingaben direkt in JavaScript einzufügen. Taghilfsprogramme codieren auch Eingaben, die Sie in Tagparametern verwenden.

Nehmen Sie die folgende Razor-Ansicht:

@{
    var untrustedInput = "<\"123\">";
}

@untrustedInput

Diese Ansicht gibt den Inhalt der Variablen untrustedInput aus. Diese Variable enthält einige Zeichen, die in XSS-Angriffen verwendet werden, nämlich <, " und >. Die Untersuchung der Quelle zeigt, dass die gerenderte Ausgabe wie folgt codiert ist:

&lt;&quot;123&quot;&gt;

Warnung

ASP.NET Core MVC bietet eine HtmlString-Klasse, die bei der Ausgabe nicht automatisch codiert wird. Dies sollte niemals in Kombination mit nicht vertrauenswürdigen Eingaben verwendet werden, da dies ein XSS-Sicherheitsrisiko darstellt.

JavaScript-Codierung mithilfe von Razor

Es kann vorkommen, dass Sie einen Wert in JavaScript einfügen möchten, um ihn in Ihrer Ansicht zu verarbeiten. Es gibt zwei Möglichkeiten, dies zu tun. Der sicherste Weg, Werte einzufügen, ist, den Wert in ein data-Attribut eines Tags einzufügen und es in Ihrem JavaScript-Code abzurufen. Beispiel:

@{
    var untrustedInput = "<script>alert(1)</script>";
}

<div id="injectedData"
     data-untrustedinput="@untrustedInput" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it
    // can lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or you can use createElement() to dynamically create document elements
    // This time we're using textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>

Das oben gezeigte Markup generiert den folgenden HTML-Code:

<div id="injectedData"
     data-untrustedinput="&lt;script&gt;alert(1)&lt;/script&gt;" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it can
    // lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or you can use createElement() to dynamically create document elements
    // This time we're using textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>

Der vorangehende Code generiert die folgende Ausgabe:

<script>alert(1)</script>
<script>alert(1)</script>
<script>alert(1)</script>

Warnung

Verketten Sie nicht vertrauenswürdige Eingaben NICHT in JavaScript, um DOM-Elemente zu erstellen, oder verwenden Sie document.write() für dynamisch generierte Inhalte.

Verwenden Sie einen der folgenden Ansätze, um zu verhindern, dass Code für DOM-basiertes XSS verfügbar gemacht wird:

  • createElement() und weisen Sie Eigenschaftswerte mit entsprechenden Methoden oder Eigenschaften wie node.textContent= oder node.InnerText= zu.
  • document.CreateTextNode() und fügen Sie es an der entsprechenden DOM-Position an.
  • element.SetAttribute()
  • element[attribute]=

Zugreifen auf Encoder im Code

Die HTML-, JavaScript- und URL-Encoder stehen Ihrem Code auf zwei Arten zur Verfügung:

  • Fügen Sie sie über die Abhängigkeitseinschleusung ein.
  • Verwenden Sie die im Namespace System.Text.Encodings.Web enthaltenen Standardencoder.

Wenn Sie die Standardencoder verwenden, werden alle Anpassungen, die auf die als sicher zu behandelnden Zeichenbereiche angewendet werden, nicht wirksam. Die Standardencoder verwenden die sichersten Codierungsregeln, die möglich sind.

Um die konfigurierbaren Encoder über DI zu verwenden, sollten Ihre Konstruktoren je nach Bedarf einen HtmlEncoder-, JavaScriptEncoder- und UrlEncoder-Parameter übernehmen. Beispiel:

public class HomeController : Controller
{
    HtmlEncoder _htmlEncoder;
    JavaScriptEncoder _javaScriptEncoder;
    UrlEncoder _urlEncoder;

    public HomeController(HtmlEncoder htmlEncoder,
                          JavaScriptEncoder javascriptEncoder,
                          UrlEncoder urlEncoder)
    {
        _htmlEncoder = htmlEncoder;
        _javaScriptEncoder = javascriptEncoder;
        _urlEncoder = urlEncoder;
    }
}

Codieren von URL-Parametern

Wenn Sie eine URL-Abfragezeichenfolge mit einer nicht vertrauenswürdigen Eingabe als Wert erstellen möchten, verwenden Sie den UrlEncoder, um den Wert zu codieren. Ein auf ein Objekt angewendeter

var example = "\"Quoted Value with spaces and &\"";
var encodedValue = _urlEncoder.Encode(example);

Nach der Codierung enthält die encodedValue-Variable den Wert %22Quoted%20Value%20with%20spaces%20and%20%26%22. Leerzeichen, Anführungszeichen, Satzzeichen und andere unsichere Zeichen werden in ihren hexadezimalen Wert codiert, z. B. wird ein Leerzeichen zu %20.

Warnung

Verwenden Sie keine nicht vertrauenswürdigen Eingaben als Teil eines URL-Pfads. Übergeben Sie nicht vertrauenswürdige Eingaben immer als Wert einer Abfragezeichenfolge.

Anpassen der Encoder

Standardmäßig verwenden Encoder eine sichere Liste, die sich auf den Unicode-Bereich grundlegender lateinischer Zeichen beschränkt, und codieren alle Zeichen außerhalb dieses Bereichs als ihre Zeichencodeäquivalente. Dieses Verhalten wirkt sich auch auf das Razor TagHelper- und HtmlHelper-Rendering aus, da es die Encoder zur Ausgabe Ihrer Zeichenfolgen verwendet.

Der Grund dafür ist der Schutz vor unbekannten oder zukünftigen Browserfehlern (frühere Browserfehler haben das Analysieren von nicht englischen Zeichen gestört). Wenn Ihre Website viele nicht lateinische Zeichen verwendet, z. B. Chinesisch, Kyrillisch oder andere Sprachen, ist dies wahrscheinlich nicht das gewünschte Verhalten.

Die für Encoder sicheren Listen können so angepasst werden, dass sie Unicode-Bereiche enthalten, die für die App während des Starts geeignet sind, in Program.cs:

Verwenden Sie z. B. die Standardkonfiguration unter Verwendung einer Razor HtmlHelper-Instanz ähnlich der folgenden:

<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>

Die vorangehende Markierung wird mit chinesischem Text codiert gerendert:

<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>

Um die vom Encoder als sicher behandelten Zeichen zu erweitern, fügen Sie die folgende Zeile in Program.cs ein:

builder.Services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));

Sie können die für Encoder sicheren Listen so anpassen, dass sie während des Starts Unicode-Bereiche enthalten, die für Ihre Anwendung geeignet sind, in ConfigureServices().

Mithilfe der Standardkonfiguration könnten Sie z. B. eine Razor HtmlHelper-Instanz wie folgt verwenden:

<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>

Wenn Sie sich den Quelltext der Webseite ansehen, sehen Sie, dass er wie folgt gerendert wurde, wobei der chinesische Text codiert wurde.

<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>

Um die vom Encoder als sicher behandelten Zeichen zu erweitern, fügen Sie die folgende Zeile in die ConfigureServices()-Methode in startup.cs ein.

services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));

In diesem Beispiel wird die sichere Liste um den Unicode-Bereich „CjkUnifiedIdeographs“ erweitert. Die gerenderte Ausgabe würde jetzt wie folgt aussehen:

<p>This link text is in Chinese: <a href="/">汉语/漢語</a></p>

Sichere Listenbereiche werden als Unicode-Zeichentabellen angegeben, nicht als Sprachen. Der Unicode-Standard enthält eine Liste von Zeichentabellen, die Sie verwenden können, um die Tabelle zu finden, die Ihre Zeichen enthält. Jeder Encoder, HTML, JavaScript und URL, muss separat konfiguriert werden.

Hinweis

Die Anpassung der sicheren Liste betrifft nur Encoder, die über DI bezogen werden. Wenn Sie über System.Text.Encodings.Web.*Encoder.Default direkt auf einen Encoder zugreifen, wird die standardmäßige, auf grundlegende lateinische Zeichen beschränkte sichere Liste verwendet.

Wo sollte die Codierung erfolgen?

Die allgemein akzeptierte Praxis ist, dass die Codierung am Ausgabepunkt erfolgt und codierte Werte niemals in einer Datenbank gespeichert werden sollten. Die Codierung am Ausgabepunkt ermöglicht es Ihnen, die Verwendung der Daten zu ändern, z. B. von HTML zu einem Wert der Abfragezeichenfolge. Außerdem können Sie Ihre Daten problemlos durchsuchen, ohne die Werte vor der Suche codieren zu müssen, und Sie können von allen Änderungen oder Fehlerkorrekturen an den Encodern profitieren.

Überprüfung als XSS-Präventionstechnik

Die Überprüfung kann ein nützliches Tool darstellen, um XSS-Angriffe zu begrenzen. Beispielsweise wird eine numerische Zeichenfolge, die nur die Zeichen 0 bis 9 enthält, keinen XSS-Angriff auslösen. Die Überprüfung wird komplizierter, wenn Sie HTML als Benutzereingabe akzeptieren. Die Analyse von HTML-Eingaben ist schwierig, wenn nicht gar unmöglich. Markdown in Verbindung mit einem Parser, der eingebettetes HTML entfernt, ist eine sicherere Option, um umfangreiche Eingaben zu akzeptieren. Verlassen Sie sich niemals allein auf die Überprüfung. Verschlüsseln Sie nicht vertrauenswürdige Eingaben immer vor der Ausgabe, unabhängig davon, welche Überprüfung oder Bereinigung durchgeführt wurde.