Impedir scripts entre sites (XSS) no ASP.NET Core
Por Rick Anderson
Cross-Site Scripting (XSS) é uma vulnerabilidade de segurança que permite que um ciberatacante coloque scripts do lado do cliente (geralmente JavaScript) em páginas da Web. Quando outros usuários carregam páginas afetadas, os scripts do ciberatacante são executados, permitindo que o ciberatacante roube cookies e tokens de sessão, altere o conteúdo da página da Web por meio de manipulação DOM ou redirecione o navegador para outra página. As vulnerabilidades XSS geralmente ocorrem quando um aplicativo leva a entrada do usuário e a envia para uma página sem validá-la, codificá-la ou escapar dela.
Este artigo se aplica principalmente ao MVC ASP.NET Core com modos de exibição, Razor Pages e outros aplicativos que retornam HTML que pode ser vulnerável ao XSS. As APIs da 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 deposita na API. Por exemplo, se uma API aceitar conteúdo gerado pelo usuário e retorná-lo em uma resposta HTML, um ciberinvasor poderá injetar scripts mal-intencionados no conteúdo que é executado quando a resposta é processada no navegador do usuário.
Para evitar ataques XSS, as APIs da Web devem implementar validação de entrada e 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 limpos para que não possam ser executados como código pelo navegador do usuário. Para obter mais informações, consulte este problema do GitHub.
Protegendo seu aplicativo contra XSS
Em um nível básico, o XSS funciona enganando seu aplicativo para inserir uma tag <script>
em sua página renderizada ou inserindo um evento On*
em um elemento. Os desenvolvedores devem usar as seguintes etapas de prevenção para evitar a introdução de XSS em seus aplicativos:
- Nunca coloque dados não confiáveis em sua entrada HTML, a menos que siga o restante das etapas abaixo. Dados não confiáveis são quaisquer dados que podem ser controlados por um ciberatacante, 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 ciberinvasor pode ser capaz de violar seu banco de dados, mesmo que não possa violar seu aplicativo.
- Antes de colocar dados não confiáveis dentro de um elemento HTML, verifique se eles estão codificados em HTML. A codificação HTML usa caracteres como < e os transforma em uma forma segura como <.
- Antes de colocar dados não confiáveis em um atributo HTML, verifique se eles estão codificados em HTML. A codificação de atributos HTML é um subconjunto da codificação HTML e codifica aspas duplas ("), aspas simples ('), e comercial (&) e caracteres menores que (<).
- Antes de colocar dados não confiáveis em JavaScript, coloque os dados em um elemento HTML cujo conteúdo você recupera em tempo de execução. Se isso não for possível, verifique se os dados estão codificados em JavaScript. A codificação JavaScript substitui caracteres perigosos por sua representação hexadecimal. Por exemplo, < seria codificado como
\u003C
. - Antes de colocar dados não confiáveis em uma cadeia de caracteres de consulta de URL, verifique se a URL está codificada.
Codificação HTML usando Razor
O mecanismo de Razor usado no MVC codifica automaticamente todas as saídas originadas de variáveis, a menos que você trabalhe muito duro para impedir que isso aconteça. Ele usa regras de codificação de atributo HTML sempre que você usa a diretiva @. Como a codificação de atributos HTML é um superconjunto de codificação HTML, isso significa que você não precisa se preocupar se deve usar a codificação HTML ou a codificação de atributos HTML. Você deve garantir que use @ apenas em um contexto HTML, não ao tentar inserir entradas não confiáveis diretamente em JavaScript. Os auxiliares de tag também codificarão a entrada que você usa nos parâmetros da tag.
Tome a seguinte visualização Razor:
@{
var untrustedInput = "<\"123\">";
}
@untrustedInput
Esta visão exibe o conteúdo da variável untrustedInput. Esta variável inclui alguns caracteres que são usados em ataques XSS, nomeadamente <, " e >. Examinando a fonte mostra a saída renderizada codificada como:
<"123">
Advertência
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 entradas não confiáveis, pois isso exporá uma vulnerabilidade de XSS.
Codificação JavaScript usando Razor
Pode haver momentos em que você queira inserir um valor no JavaScript para processar em sua visualização. Há duas maneiras de fazer isso. A maneira mais segura de inserir valores é colocar o valor em um atributo de dados de uma tag 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="<script>alert(1)</script>" />
<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>
Advertência
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 evitar que o código seja exposto ao XSS baseado em DOM:
-
createElement()
e atribua valores a propriedades usando métodos ou propriedades apropriados, comonode.textContent=
ounode.InnerText=
. -
document.CreateTextNode()
e anexe-o na localização adequada no DOM. element.SetAttribute()
element[attribute]=
Aceder a codificadores em código
Os codificadores HTML, JavaScript e URL estão disponíveis para o seu código de duas maneiras:
- Injete-os por meio de injeção de dependência através de .
- Use os codificadores padrão contidos no namespace
System.Text.Encodings.Web
.
Ao usar os codificadores padrão, quaisquer personalizações aplicadas a intervalos de caracteres para serem tratadas como seguras não terão efeito. Os codificadores padrão usam as regras de codificação mais seguras possíveis.
Para usar os codificadores configuráveis via DI, seus construtores devem usar um HtmlEncoder, JavaScriptEncoder e parâmetro 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;
}
}
Codificando parâmetros de URL
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 inseguros são codificados percentualmente para seu valor hexadecimal, por exemplo, um caractere de espaço se tornará %20.
Advertência
Não use entradas não confiáveis como parte de um caminho de URL. Sempre passe entrada não confiável como um valor de parâmetro 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 seus equivalentes de código de caracteres. Esse comportamento também afeta Razor renderização TagHelper e HtmlHelper como ele usa os codificadores para saída suas cadeias de caracteres.
O raciocínio por trás disso é proteger contra bugs de navegador desconhecidos ou futuros (bugs anteriores do navegador causaram problemas na análise, baseado no processamento de caracteres pertencentes a línguas não inglesas). Se o seu site faz uso intensivo de caracteres não latinos, como chinês, cirílico ou outros, este provavelmente não é o comportamento que você deseja.
As listas de segurança do codificador podem ser personalizadas para incluir intervalos Unicode adequados ao aplicativo durante a inicialização, de Program.cs
:
Por exemplo, usando a configuração padrão usando um Razor HtmlHelper semelhante ao seguinte:
<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>
A marcação anterior é processada com texto chinês codificado:
<p>This link text is in Chinese: <a href="/">汉语/漢語</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 Razor HtmlHelper assim;
<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>
Ao visualizar a fonte da página da web, verá que ela foi processada da seguinte forma, com o texto chinês codificado.
<p>This link text is in Chinese: <a href="/">汉语/漢語</a></p>
Para ampliar os caracteres tratados como seguros pelo codificador, você deve inserir a seguinte linha 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 Unicode Range CjkUnifiedIdeographs. A saída renderizada passaria a ser
<p>This link text is in Chinese: <a href="/">汉语/漢語</a></p>
Os intervalos de listas seguras são especificados como gráficos de código Unicode, não idiomas. O padrão Unicode
Observação
A personalização da lista segura afeta apenas os codificadores provenientes via DI. Se você acessar diretamente um codificador via System.Text.Encodings.Web.*Encoder.Default
então a lista segura padrão, Basic Latin only será usada.
Onde deve ocorrer a codificação?
A prática geralmente aceite é que a codificação ocorre no ponto de saída e os valores codificados nunca devem ser armazenados numa base de dados. A codificação no ponto de saída permite alterar 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 ter que codificar valores antes de pesquisar e permite que você aproveite quaisquer alterações ou correções de bugs feitas nos codificadores.
Validação como técnica de prevenção de XSS
A validação pode ser uma ferramenta útil para limitar ataques XSS. Por exemplo, uma cadeia de caracteres numérica contendo apenas os caracteres 0-9 não acionará um ataque XSS. A validação torna-se mais complicada ao aceitar HTML na entrada do usuário. Analisar a entrada HTML é difícil, se não impossível. Markdown, juntamente com um analisador que remove HTML incorporado, é uma opção mais segura para aceitar entradas avançadas. Nunca confie apenas na validação. Sempre codifique entradas não confiáveis antes da saída, independentemente da validação ou higienização realizada.