Evitar XSS (Cross-Site Scripting) no ASP.NET Core

De Rick Anderson

O XSS (Cross-Site Scripting) é uma vulnerabilidade de segurança que permite que um invasor coloque scripts do lado do cliente (geralmente JavaScript) em páginas da Web. Quando outros usuários carregam páginas afetadas, os scripts do invasor são executados, permitindo que o invasor roube cookie e tokens de sessão, altere o conteúdo da página da Web por meio da manipulação do DOM ou redirecione o navegador para outra página. Vulnerabilidades XSS geralmente ocorrem quando um aplicativo usa a entrada do usuário e a gera em uma página sem validar, codificar ou escapar dela.

Este artigo se aplica principalmente a ASP.NET Core MVC com exibições, Razor Pages e outros aplicativos que retornam HTML que podem ser vulneráveis ao XSS. AS APIs Web que retornam dados na forma de HTML, XML ou JSON podem disparar ataques XSS em seus aplicativos cliente se não limparem corretamente a entrada do usuário, dependendo da confiança que o aplicativo cliente coloca na API. Por exemplo, se uma API aceitar conteúdo gerado pelo usuário e o retornar em uma resposta HTML, um invasor poderá injetar scripts mal-intencionados no conteúdo executado quando a resposta for renderizada no navegador do usuário.

Para evitar ataques XSS, as APIs Web devem implementar a validação de entrada e a codificação de saída. A validação de entrada garante que a entrada do usuário atenda aos critérios esperados e não inclua código mal-intencionado. A codificação de saída garante que todos os dados retornados pela API sejam devidamente higienizados para que não possam ser executados como código pelo navegador do usuário. Saiba mais neste tópico do GitHub.

Protegendo seu aplicativo contra XSS

A nível básico, o XSS funciona enganando seu aplicativo para inserir uma marca <script> na página renderizada ou para inserir um evento On* em um elemento. Os desenvolvedores devem usar as seguintes etapas de prevenção para evitar a introdução do XSS em seus aplicativos:

  1. Nunca coloque dados não confiáveis em sua entrada HTML, a menos que você siga o restante das etapas abaixo. Dados não confiáveis são todos os dados que podem ser controlados por um invasor, como entradas de formulário HTML, cadeias de caracteres de consulta, cabeçalhos HTTP ou até mesmo dados provenientes de um banco de dados, pois um invasor pode violar seu banco de dados, mesmo que não possa violar seu aplicativo.

  2. Antes de colocar dados não confiáveis dentro de um elemento HTML, verifique se eles são codificados em HTML. A codificação HTML usa caracteres como < e os altera em uma forma segura como <

  3. Antes de colocar dados não confiáveis em um atributo HTML, verifique se eles são codificados em HTML. A codificação de atributo HTML é um superconjunto de codificação HTML e codifica caracteres adicionais, como " e ".

  4. Antes de colocar dados não confiáveis em JavaScript, coloque os dados em um elemento HTML cujo conteúdo você recupera em runtime. Se isso não for possível, verifique se os dados são codificados em JavaScript. A codificação JavaScript usa caracteres perigosos para JavaScript e os substitui por seu hexadecimal, por exemplo, < seria codificado como \u003C.

  5. Antes de colocar dados não confiáveis em uma cadeia de caracteres de consulta de URL, verifique se eles estão codificados na URL.

Codificação HTML usando Razor

O mecanismo Razor usado no MVC codifica automaticamente todas as saídas provenientes de variáveis, a menos que você se esforce muito para impedir que ele faça isso. Ele usa regras de codificação de atributo HTML sempre que você usa a diretiva @. Como a codificação de atributo HTML é um superconjunto de codificação HTML, isso significa que você não precisa se preocupar se deve usar codificação HTML ou codificação de atributo HTML. Você deve garantir que só use @ em um contexto HTML, não ao tentar inserir entrada não confiável diretamente no JavaScript. Os auxiliares de marca também codificarão a entrada usada em parâmetros de marca.

Veja a seguinte exibição Razor:

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

@untrustedInput

Essa exibição gera o conteúdo da variável untrustedInput. Essa variável inclui alguns caracteres que são usados em ataques XSS, ou seja <, " e >. Examinar a origem mostra a saída renderizada codificada como:

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

Aviso

ASP.NET Core MVC fornece uma classe HtmlString que não é codificada automaticamente na saída. Isso nunca deve ser usado em combinação com entrada não confiável, pois isso exporá uma vulnerabilidade XSS.

Codificação JavaScript usando Razor

Pode haver ocasiões em que você deseja inserir um valor em JavaScript para processar em sua exibição. Há duas maneiras de fazer isso. A maneira mais segura de inserir valores é colocar o valor em um atributo de dados de uma marca e recuperá-lo em seu JavaScript. Por exemplo:

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

A marcação anterior gera o seguinte HTML:

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

O código anterior gera a seguinte saída:

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

Aviso

NÃO concatene entradas não confiáveis em JavaScript para criar elementos DOM ou use document.write() em conteúdo gerado dinamicamente.

Use uma das seguintes abordagens para impedir que o código seja exposto ao XSS baseado em DOM:

  • createElement() e atribua valores de propriedade com métodos ou propriedades apropriados, como node.textContent= ou node.InnerText=.
  • document.CreateTextNode() e acrescente-o no local apropriado do DOM.
  • element.SetAttribute()
  • element[attribute]=

Acessando codificadores no código

Os codificadores HTML, JavaScript e URL estão disponíveis para seu código de duas maneiras:

  • Injete-os por meio de injeção de dependência.
  • Use os codificadores padrão contidos no namespace System.Text.Encodings.Web.

Ao usar os codificadores padrão, as personalizações aplicadas a intervalos de caracteres a serem tratados como seguros não entrarão em vigor. Os codificadores padrão usam as regras de codificação mais seguras possíveis.

Para usar os codificadores configuráveis por meio da DI, seus construtores devem usar um parâmetro HtmlEncoder, JavaScriptEncoder e UrlEncoder conforme apropriado. Por exemplo:

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

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

Parâmetros de URL de codificação

Se você quiser criar uma cadeia de caracteres de consulta de URL com entrada não confiável como um valor, use o UrlEncoder para codificar o valor. Por exemplo,

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

Depois de codificar a variável encodedValue contém %22Quoted%20Value%20with%20spaces%20and%20%26%22. Espaços, aspas, pontuação e outros caracteres não seguros são codificados por porcentagem para seu valor hexadecimal, por exemplo, um caractere de espaço se tornará %20.

Aviso

Não use entrada não confiável como parte de um caminho de URL. Sempre passe uma entrada não confiável como um valor de cadeia de caracteres de consulta.

Personalizando os codificadores

Por padrão, os codificadores usam uma lista segura limitada ao intervalo Unicode Latino Básico e codificam todos os caracteres fora desse intervalo como equivalentes de código de caractere. Esse comportamento também afeta a renderização de TagHelper e HtmlHelper Razor, pois usa os codificadores para gerar suas cadeias de caracteres.

O raciocínio por trás disso é proteger contra bugs desconhecidos ou futuros do navegador (bugs anteriores do navegador foram corrigidos com base no processamento de caracteres que não são em inglês). Se seu site faz uso intenso de caracteres não latinos, como chinês, cirílico ou outros, provavelmente esse não é o comportamento desejado.

As listas seguras do codificador podem ser personalizadas para incluir intervalos Unicode apropriados para o aplicativo durante a inicialização, em Program.cs:

Por exemplo, usando a configuração padrão usando um HtmlHelper Razor semelhante ao seguinte:

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

A marcação anterior é renderizada com texto em chinês codificado:

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

Para ampliar os caracteres tratados como seguros pelo codificador, insira a seguinte linha em Program.cs.:

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

Você pode personalizar as listas seguras do codificador para incluir intervalos Unicode apropriados ao seu aplicativo durante a inicialização, em ConfigureServices().

Por exemplo, usando a configuração padrão, você pode usar um HtmlHelper Razor da seguinte maneira;

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

Ao exibir a origem da página da Web, você verá que ela foi renderizada da seguinte maneira, com o texto chinês codificado;

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

Para ampliar os caracteres tratados como seguros pelo codificador, você inseriria a linha a seguir no método ConfigureServices() em startup.cs;

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

Este exemplo amplia a lista segura para incluir o Intervalo Unicode CjkUnifiedIdeographs. A saída renderizada agora se tornaria

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

Intervalos de lista seguros são especificados como gráficos de código Unicode, não idiomas. O padrão Unicode tem uma lista de gráficos de código que você pode usar para localizar o gráfico que contém seus caracteres. Cada codificador, Html, JavaScript e URL, deve ser configurado separadamente.

Observação

A personalização da lista segura afeta apenas os codificadores originados por meio da DI. Se você acessar diretamente um codificador por meio de System.Text.Encodings.Web.*Encoder.Default e então do padrão, a lista de segurança somente latina básica será usada.

Onde a codificação deve ocorrer?

A prática geral aceita é que a codificação ocorre no ponto de saída e os valores codificados nunca devem ser armazenados em um banco de dados. A codificação no ponto de saída permite que você altere o uso de dados, por exemplo, de HTML para um valor de cadeia de caracteres de consulta. Ele também permite que você pesquise facilmente seus dados sem precisar codificar valores antes de pesquisar e permite que você aproveite as alterações ou correções de bug feitas aos codificadores.

Validação como uma técnica de prevenção XSS

A validação pode ser uma ferramenta útil para limitar ataques XSS. Por exemplo, uma cadeia de caracteres numérica que contém apenas os caracteres 0-9 não disparará um ataque XSS. A validação torna-se mais complicada ao aceitar HTML na entrada do usuário. A análise da entrada HTML é difícil, se não impossível. Markdown, juntamente com um analisador que distribui HTML inserido, é uma opção mais segura para aceitar entradas avançadas. Nunca confie apenas na validação. Sempre codifique a entrada não confiável antes da saída, independentemente de qual validação ou limpeza tenha sido executada.