Empêcher les scripts intersites (XSS) dans ASP.NET Core

Par Rick Anderson

Cross-Site Scripting (XSS) est une vulnérabilité de sécurité qui permet à un attaquant de placer des scripts côté client (généralement JavaScript) dans des pages Web. Lorsque d'autres utilisateurs chargent des pages affectées, les scripts de l'attaquant s'exécutent, permettant à l'attaquant de voler des cookies et des jetons de session, de modifier le contenu de la page Web par manipulation DOM ou de rediriger le navigateur vers une autre page. Les vulnérabilités XSS se produisent généralement lorsqu'une application prend une entrée utilisateur et la sort sur une page sans la valider, l'encoder ou l'échapper.

Cet article s'applique principalement à ASP.NET Core MVC avec des vues, des Pages Razor et d'autres applications qui renvoient du code HTML susceptible d'être vulnérable à XSS. Les API Web qui renvoient des données au format HTML, XML ou JSON peuvent déclencher des attaques XSS dans leurs applications clientes si elles ne nettoient pas correctement les entrées utilisateur, en fonction du degré de confiance que l'application cliente accorde à l'API. Par exemple, si une API accepte le contenu généré par l'utilisateur et le renvoie dans une réponse HTML, un attaquant pourrait injecter des scripts malveillants dans le contenu qui s'exécute lorsque la réponse est rendue dans le navigateur de l'utilisateur.

Pour empêcher les attaques XSS, les API Web doivent implémenter la validation des entrées et le codage des sorties. La validation des entrées garantit que l'entrée de l'utilisateur répond aux critères attendus et n'inclut pas de code malveillant. L'encodage de sortie garantit que toutes les données renvoyées par l'API sont correctement filtrées afin qu'elles ne puissent pas être exécutées en tant que code par le navigateur de l'utilisateur. Pour plus d’informations, consultez ce problème GitHub.

Protéger votre application contre XSS

À la base, XSS fonctionne en incitant votre application à insérer une balise <script> dans votre page rendue ou en insérant un événement On* dans un élément. Les développeurs doivent suivre les étapes de prévention suivantes pour éviter d'introduire XSS dans leurs applications :

  1. Ne mettez jamais de données non fiables dans votre entrée HTML, sauf si vous suivez le reste des étapes ci-dessous. Les données non fiables sont toutes les données qui peuvent être contrôlées par un attaquant, telles que les entrées de formulaire HTML, les chaînes de requête, les en-têtes HTTP ou même les données provenant d'une base de données, car un attaquant peut être en mesure de violer votre base de données même s'il ne peut pas violer ton application.

  2. Avant de placer des données non fiables dans un élément HTML, assurez-vous qu'il est encodé en HTML. L’encodage HTML prend des caractères comme < et leur applique un format sécurisé (par exemple, <).

  3. Avant de placer des données non fiables dans un attribut HTML, assurez-vous qu'il est encodé en HTML. L'encodage d'attribut HTML est un sur-ensemble de l'encodage HTML et encode des caractères supplémentaires tels que " et ".

  4. Avant de placer des données non fiables dans JavaScript, placez les données dans un élément HTML dont vous récupérez le contenu au moment de l'exécution. Si cela n'est pas possible, assurez-vous que les données sont encodées en JavaScript. L'encodage JavaScript prend des caractères dangereux pour JavaScript et les remplace par leur hexadécimal, par exemple, < serait encodé en tant que \u003C.

  5. Avant de placer des données non fiables dans une chaîne de requête d'URL, assurez-vous qu'elle est codée en URL.

Encodage HTML à l'aide Razor

Le moteur Razor utilisé dans MVC encode automatiquement toutes les sorties provenant de variables, à moins que vous ne travailliez très dur pour l'empêcher de le faire. Il utilise les règles d'encodage des attributs HTML chaque fois que vous utilisez la directive @. Comme l'encodage d'attribut HTML est un sur-ensemble de l'encodage HTML, cela signifie que vous n'avez pas à vous soucier de savoir si vous devez utiliser l'encodage HTML ou l'encodage d'attribut HTML. Vous devez vous assurer que vous n'utilisez @ que dans un contexte HTML, et non lorsque vous essayez d'insérer une entrée non fiable directement dans JavaScript. Les assistants de balise encoderont également l'entrée que vous utilisez dans les paramètres de balise.

Prenez la vue suivante Razor :

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

@untrustedInput

Cette vue génère le contenu de la variable untrustedInput. Cette variable inclut certains caractères utilisés dans les attaques XSS, à savoir <, " et >. L'examen de la source montre la sortie rendue encodée comme :

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

Avertissement

ASP.NET Core MVC fournit une classe HtmlString qui n'est pas automatiquement encodée lors de la sortie. Cela ne doit jamais être utilisé en combinaison avec une entrée non fiable car cela exposerait une vulnérabilité XSS.

Encodage JavaScript à l'aide Razor

Il peut arriver que vous souhaitiez insérer une valeur dans JavaScript à traiter dans votre vue. Il existe deux façons d'effectuer cette opération. Le moyen le plus sûr d'insérer des valeurs consiste à placer la valeur dans un attribut de données d'une balise et à la récupérer dans votre JavaScript. Par exemple :

@{
    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>

Le balisage précédent génère le code HTML suivant :

<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>

Le code précédent génère la sortie suivante :

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

Avertissement

Ne concaténez PAS les entrées non fiables dans JavaScript pour créer des éléments DOM ou les utiliser document.write() sur du contenu généré dynamiquement.

Utilisez l'une des approches suivantes pour empêcher le code d'être exposé à XSS basé sur DOM :

  • createElement() et affectez des valeurs de propriété avec des méthodes ou des propriétés appropriées telles que node.textContent= ou node.InnerText=.
  • document.CreateTextNode() et ajoutez-le à l'emplacement DOM approprié.
  • element.SetAttribute()
  • element[attribute]=

Accéder aux encodeurs dans le code

Les encodeurs HTML, JavaScript et URL sont disponibles pour votre code de deux manières :

  • Injectez-les via l'injection de dépendances.
  • Utilisez les encodeurs par défaut contenus dans l'espace de noms System.Text.Encodings.Web.

Lorsque vous utilisez les encodeurs par défaut, toutes les personnalisations appliquées aux plages de caractères à traiter comme sûres ne prendront pas effet. Les encodeurs par défaut utilisent les règles d'encodage les plus sûres possibles.

Pour utiliser les encodeurs configurables via DI, vos constructeurs doivent prendre un paramètre HtmlEncoder, JavaScriptEncoder et UrlEncoder selon le cas. Par exemple :

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

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

Encodage des paramètres d'URL

Si vous souhaitez créer une chaîne de requête d'URL avec une entrée non fiable comme valeur, utilisez le UrlEncoder pour coder la valeur. Par exemple,

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

Après encodage, la variable encodedValue contient %22Quoted%20Value%20with%20spaces%20and%20%26%22. Les espaces, les guillemets, la ponctuation et les autres caractères non sécurisés sont codés en pourcentage à leur valeur hexadécimale, par exemple un caractère d'espace deviendra %20.

Avertissement

N'utilisez pas d'entrée non fiable dans le cadre d'un chemin d'URL. Transmettez toujours une entrée non fiable en tant que valeur de chaîne de requête.

Personnalisation des encodeurs

Par défaut, les encodeurs utilisent une liste sécurisée limitée à la plage De base Latin Unicode et encodent tous les caractères en dehors de cette plage comme leurs équivalents de code de caractères. Ce comportement affecte également TagHelper Razor et HtmlHelper car il utilise les encodeurs pour générer vos chaînes.

Le raisonnement derrière cela est de se protéger contre les bogues de navigateur inconnus ou futurs (les bogues de navigateur précédents ont déclenché l'analyse basée sur le traitement de caractères non anglais). Si votre site Web utilise beaucoup de caractères non latins, tels que le chinois, le cyrillique ou autres, ce n'est probablement pas le comportement que vous souhaitez.

Les listes sûres de l'encodeur peuvent être personnalisées pour inclure des plages Unicode appropriées à l'application lors du démarrage, dans Program.cs :

Par exemple, en utilisant la configuration par défaut à l'aide d’un HtmlHelper Razor similaire à ce qui suit :

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

Le balisage précédent est rendu avec du texte chinois encodé :

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

Pour élargir les caractères traités comme sûrs par l'encodeur, insérez la ligne suivante dans Program.cs. :

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

Vous pouvez personnaliser les listes sûres de l'encodeur pour inclure des plages Unicode appropriées à votre application lors du démarrage, dans ConfigureServices().

Par exemple, en utilisant la configuration par défaut, vous pouvez utiliser un HtmlHelper Razor comme ceci ;

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

Lorsque vous affichez la source de la page Web, vous verrez qu'elle a été rendue comme suit, avec le texte chinois encodé ;

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

Pour élargir les caractères traités comme sûrs par l'encodeur, vous insérez la ligne suivante dans la méthode ConfigureServices() dans startup.cs ;

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

Cet exemple élargit la liste sûre pour inclure la plage Unicode CjkUnifiedIdeographs. La sortie rendue deviendrait maintenant

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

Les plages de listes sûres sont spécifiées sous forme de tableaux de code Unicode, et non de langues. La norme Unicode contient une liste de tableaux de codes que vous pouvez utiliser pour trouver le tableau contenant vos caractères. Chaque encodeur, Html, JavaScript et Url, doit être configuré séparément.

Notes

La personnalisation de la liste sûre n'affecte que les codeurs alimentés via DI. Si vous accédez directement à un encodeur via la liste sécurisée System.Text.Encodings.Web.*Encoder.Default par défaut, De base Latin uniquement sera utilisée.

Où doit avoir lieu l'encodage ?

La pratique généralement acceptée est que le codage a lieu au point de sortie et que les valeurs codées ne doivent jamais être stockées dans une base de données. Le codage au point de sortie vous permet de modifier l'utilisation des données, par exemple, de HTML à une valeur de chaîne de requête. Il vous permet également de rechercher facilement vos données sans avoir à encoder les valeurs avant la recherche et vous permet de profiter de toutes les modifications ou corrections de bogues apportées aux encodeurs.

Validation en tant que technique de prévention XSS

La validation peut être un outil utile pour limiter les attaques XSS. Par exemple, une chaîne numérique contenant uniquement les caractères 0 à 9 ne déclenchera pas d'attaque XSS. La validation devient plus compliquée lors de l'acceptation du HTML dans l'entrée de l'utilisateur. L'analyse de l'entrée HTML est difficile, voire impossible. Markdown, associé à un analyseur qui supprime le HTML intégré, est une option plus sûre pour accepter une entrée riche. Ne comptez jamais uniquement sur la validation. Encodez toujours les entrées non fiables avant les sorties, quelle que soit la validation ou le nettoyage effectué.