Evitar XSS (Cross-Site Scripting) no ASP.NET Core
Cross-Site Scripting (XSS) é uma vulnerabilidade de segurança que permite a um ciberatacante inserir scripts do lado do cliente (geralmente JavaScript) em páginas web. Quando outros usuários carregam as páginas afetadas, os scripts do ciberatacante são executados, possibilitando o roubo de cookies e tokens de sessão, a alteração do conteúdo da página via manipulação do DOM ou o redirecionamento do navegador para outra página. Vulnerabilidades de XSS geralmente ocorrem quando uma aplicação recebe dados de entrada do usuário e os exibe em uma página sem validá-los, codificá-los ou escapá-los.
Este artigo se aplica principalmente ao ASP.NET Core MVC com views, Razor Pages e outros aplicativos que retornam HTML que podem estar vulneráveis a XSS. APIs web que retornam dados em formato de HTML, XML ou JSON podem desencadear ataques XSS em seus aplicativos clientes se não sanitizarem corretamente os dados de entrada, dependendo do grau de confiança que o aplicativo cliente deposita na API. Por exemplo, se uma API aceita conteúdo gerado pelo usuário e o retorna em uma resposta HTML, um ciberatacante poderia injetar scripts maliciosos nesse conteúdo, os quais seriam executados quando a resposta fosse renderizada no navegador do usuário.
Para prevenir ataques XSS, as APIs web devem implementar validação de entrada e codificação de saída. A validação de entrada garante que os dados fornecidos pelo usuário atendam aos critérios esperados e não incluam código malicioso. A codificação de saída assegura que quaisquer dados retornados pela API sejam devidamente sanitizados, de modo que não possam ser executados como código no navegador do usuário. Para mais informações, consulte esta [issue do GitHub].
Protegendo sua aplicação contra XSS
De forma básica, o XSS funciona enganando sua aplicação para inserir uma tag <script>
na página renderizada ou inserindo um evento On*
em um elemento. Os desenvolvedores devem seguir os seguintes passos de prevenção para evitar a introdução de XSS em suas aplicações:
- Nunca insira dados não confiáveis em sua entrada HTML, a menos que siga os demais passos abaixo. Dados não confiáveis são quaisquer informações que possam ser controladas por um ciberatacante, como entradas de formulários HTML, query strings, cabeçalhos HTTP ou mesmo dados oriundos de um banco de dados, pois um ciberatacante pode conseguir violar seu banco de dados mesmo que não consiga invadir sua aplicação.
- Antes de inserir dados não confiáveis dentro de um elemento HTML, certifique-se de que eles estejam codificados em HTML. A codificação HTML converte caracteres como
<
em uma forma segura, como<
. - Antes de inserir dados não confiáveis em um atributo HTML, assegure-se de que eles estejam codificados. A codificação de atributos HTML é um subconjunto da codificação HTML e converte aspas duplas (
"
), aspas simples ('
), o e comercial (&
) e o caractere menor que (<
). - Antes de inserir dados não confiáveis em JavaScript, coloque os dados em um elemento HTML cujo conteúdo será recuperado em tempo de execução. Se isso não for possível, certifique-se de que os dados estejam codificados em JavaScript. A codificação JavaScript converte caracteres perigosos e os substitui por seus equivalentes hexadecimais, por exemplo,
<
seria codificado como\u003C
. - Antes de inserir dados não confiáveis em uma query string de URL, certifique-se de que eles estejam codificados para URL.
Codificação HTML usando Razor
O motor Razor utilizado no MVC codifica automaticamente toda saída oriunda de variáveis, a menos que você se esforce para evitar isso. Ele utiliza as regras de codificação de atributos HTML sempre que a diretiva @
é empregada. Como a codificação de atributos HTML é um superconjunto da codificação HTML, você não precisa se preocupar se deve usar uma ou outra. Apenas assegure-se de utilizar o @
em um contexto HTML, e não ao tentar inserir dados não confiáveis diretamente em JavaScript. Os Tag Helpers também codificam as entradas utilizadas em parâmetros de tag.
Considere a seguinte view Razor:
CSHTML
cshtml
CopiarEditar
@{
var untrustedInput = "<\"123\">";
}
@untrustedInput
Esta view exibe o conteúdo da variável untrustedInput
. Essa variável inclui alguns caracteres usados em ataques XSS, como <
, "
e >
. Ao examinar o código-fonte, percebe-se que a saída renderizada está codificada da seguinte forma:
HTML
html
CopiarEditar
<"123">
Aviso
O ASP.NET Core MVC fornece uma classe HtmlString
que não é codificada automaticamente na saída. Isto nunca deve ser usado em conjunto com dados não confiáveis, pois exporia uma vulnerabilidade XSS.
Codificação JavaScript usando Razor
Pode haver momentos em que você deseja inserir um valor em JavaScript para processá-lo em sua view. Existem duas formas de fazer isso. A maneira mais segura de inserir valores é colocar o valor em um atributo data
de uma tag e recuperá-lo em seu JavaScript. Por exemplo:
CSHTML
cshtml
CopiarEditar
@{
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");
// Todos os clientes
var clientSideUntrustedInputOldStyle =
injectedData.getAttribute("data-untrustedinput");
// Apenas clientes HTML5
var clientSideUntrustedInputHtml5 =
injectedData.dataset.untrustedinput;
// Insere os dados não confiáveis injetados na tag div 'scriptedWrite'.
// NÃO use document.write() com dados gerados dinamicamente, pois
// pode levar a XSS.
document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;
// Ou você pode usar createElement() para criar dinamicamente elementos do documento.
// Desta vez, estamos usando textContent para garantir que os dados sejam codificados corretamente.
var x = document.createElement("div");
x.textContent = clientSideUntrustedInputHtml5;
document.body.appendChild(x);
// Você também pode usar createTextNode em um elemento para garantir que os dados sejam codificados corretamente.
var y = document.createElement("div");
y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
document.body.appendChild(y);
</script>
A marcação acima gera o seguinte HTML:
HTML
html
CopiarEditar
<div id="injectedData"
data-untrustedinput="<script>alert(1)</script>" />
<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />
<script>
var injectedData = document.getElementById("injectedData");
// Todos os clientes
var clientSideUntrustedInputOldStyle =
injectedData.getAttribute("data-untrustedinput");
// Apenas clientes HTML5
var clientSideUntrustedInputHtml5 =
injectedData.dataset.untrustedinput;
// Insere os dados não confiáveis injetados na tag div 'scriptedWrite'.
// NÃO use document.write() com dados gerados dinamicamente, pois
// pode levar a XSS.
document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;
// Ou você pode usar createElement() para criar dinamicamente elementos do documento.
// Desta vez, estamos usando textContent para garantir que os dados sejam codificados corretamente.
var x = document.createElement("div");
x.textContent = clientSideUntrustedInputHtml5;
document.body.appendChild(x);
// Você também pode usar createTextNode em um elemento para garantir que os dados sejam codificados corretamente.
var y = document.createElement("div");
y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
document.body.appendChild(y);
</script>
O código acima gera a seguinte saída:
html
CopiarEditar
<script>alert(1)</script>
<script>alert(1)</script>
<script>alert(1)</script>
Aviso
NÃO concatene dados não confiáveis em JavaScript para criar elementos do DOM ou use document.write() em conteúdo gerado dinamicamente.
Utilize uma das abordagens a seguir para evitar que o código seja exposto a ataques XSS baseados em DOM:
createElement()
e atribua valores às propriedades utilizando métodos ou propriedades apropriadas, comonode.textContent=
ounode.InnerText=
.document.CreateTextNode()
e anexe-o na localização adequada do DOM.element.SetAttribute()
element[attribute]=
Acessando codificadores no código
Os codificadores de HTML, JavaScript e URL estão disponíveis para o seu código de duas maneiras:
- Injete-os via injeção de dependência (DI).
- Utilize os codificadores padrão contidos no namespace
System.Text.Encodings.Web
.
Ao utilizar os codificadores padrão, quaisquer personalizações aplicadas aos intervalos de caracteres considerados seguros não terão efeito. Os codificadores padrão utilizam as regras de codificação mais seguras possíveis.
Para utilizar os codificadores configuráveis via DI, seus construtores devem receber parâmetros do tipo HtmlEncoder
, JavaScriptEncoder
e UrlEncoder
, conforme apropriado. Por exemplo:
C#
csharp
CopiarEditar
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ê deseja construir uma query string de URL utilizando dados não confiáveis como valor, utilize o UrlEncoder
para codificar o valor. Por exemplo:
C#
csharp
CopiarEditar
var example = "\"Quoted Value with spaces and &\"";
var encodedValue = _urlEncoder.Encode(example);
Após a codificação, a variável encodedValue
conterá %22Quoted%20Value%20with%20spaces%20and%20%26%22
. Espaços, aspas, pontuações e outros caracteres inseguros são codificados percentualmente para seus valores hexadecimais; por exemplo, um espaço se torna %20
.
Aviso
Não utilize dados não confiáveis como parte do caminho de uma URL. Sempre passe dados não confiáveis como valor de query string.
Personalizando os Codificadores
Por padrão, os codificadores utilizam uma lista segura limitada ao intervalo Unicode do Latin Básico e codificam todos os caracteres fora desse intervalo como seus equivalentes em código de caractere. Esse comportamento também afeta a renderização de TagHelper e HtmlHelper do Razor, pois estes utilizam os codificadores para exibir suas strings.
A razão por trás disso é proteger contra bugs desconhecidos ou futuros dos navegadores (bugs anteriores dos navegadores já atrapalharam o parsing com base no processamento de caracteres não ingleses). Se o seu site utiliza intensamente caracteres não latinos, como chinês, cirílico ou outros, esse provavelmente não é o comportamento desejado.
As listas seguras dos codificadores podem ser personalizadas para incluir intervalos Unicode apropriados para a aplicação durante a inicialização, no Program.cs
:
Por exemplo, utilizando a configuração padrão com um Razor HtmlHelper semelhante ao seguinte:
HTML
html
CopiarEditar
<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>
A marcação acima é renderizada com o texto chinês codificado:
HTML
html
CopiarEditar
<p>This link text is in Chinese: <a href="/">汉语/漢語</a></p>
Para ampliar os caracteres considerados seguros pelo codificador, insira a seguinte linha em Program.cs
:
C#
csharp
CopiarEditar
builder.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 seria:
HTML
html
CopiarEditar
<p>This link text is in Chinese: <a href="/">汉语/漢語</a></p>
Os intervalos da lista segura são especificados como tabelas de códigos Unicode, e não por línguas. O padrão Unicode possui uma lista de tabelas de códigos que você pode usar para encontrar a tabela contendo os seus caracteres. Cada codificador — HTML, JavaScript e URL — deve ser configurado separadamente.
Observação
A personalização da lista segura afeta apenas os codificadores obtidos via DI. Se você acessar diretamente um codificador via System.Text.Encodings.Web.*Encoder.Default
, será utilizada a lista segura padrão, que inclui apenas o Latin Básico.
Onde a codificação deve ocorrer?
A prática geralmente aceita é que a codificação ocorra no ponto de saída, e os valores codificados nunca devem ser armazenados em um banco de dados. Codificar no ponto de saída permite que você altere o uso dos dados, por exemplo, de HTML para um valor de query string. Além disso, isso possibilita que você pesquise seus dados facilmente, sem precisar codificar os valores antes da busca, e permite aproveitar 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 string numérica contendo apenas os caracteres de 0 a 9 não disparará um ataque XSS. No entanto, a validação torna-se mais complicada ao aceitar HTML na entrada do usuário. Analisar a entrada de HTML é difícil, senão impossível. O uso de Markdown, juntamente com um parser que remova o HTML embutido, é uma opção mais segura para aceitar entradas ricas. Nunca confie apenas na validação. Sempre codifique os dados não confiáveis antes de exibi-los, independentemente de qual validação ou sanitização tenha sido realizada.