Prevención del scripting entre sitios (XSS) en ASP.NET Core

Por Rick Anderson

El scripting entre sitios (XSS) es una vulnerabilidad que permite a un atacante colocar scripts en el lado cliente (normalmente a través de JavaScript) en páginas web. Los scripts del atacante se ejecutan cuando otros usuarios cargan páginas afectadas, lo que permite al atacante robar cookies y tokens de sesión y cambiar el contenido de la página web a través de la manipulación de DOM o redirigir el navegador a otra página. Por lo general, las vulnerabilidades XSS se producen cuando una aplicación toma una entrada del usuario y la envía a una página sin validarla, codificarla ni escaparla.

El ámbito principal de este artículo es ASP.NET Core MVC con vistas, páginas de Razor y otras aplicaciones que devuelven HTML potencialmente vulnerable a XSS. Las API web que devuelven datos en forma de HTML, XML o JSON pueden desencadenar ataques XSS en sus aplicaciones cliente si no sanean correctamente la entrada del usuario, en función de la confianza que coloca la aplicación cliente en la API. Por ejemplo, si una API acepta contenido generado por el usuario y lo devuelve en una respuesta HTML, un atacante podría insertar scripts malintencionados en el contenido que se ejecuta cuando la respuesta se representa en el explorador del usuario.

Para evitar ataques XSS, las API web deben implementar la validación de entrada y la codificación de salida. La validación de entrada garantiza que la entrada del usuario cumpla los criterios esperados y no incluya código malintencionado. La codificación de salida garantiza que los datos devueltos por la API se saneen correctamente para que el explorador del usuario no pueda ejecutarlos como código. Para más información, consulte este problema de GitHub.

Protección de la aplicación frente a XSS

A nivel básico, XSS engaña a la aplicación para que inserte una etiqueta <script> en la página representada o un evento On* en un elemento. Los desarrolladores deben usar los siguientes pasos de prevención para evitar que se introduzca XSS en sus aplicaciones:

  1. Nunca coloque datos que no sean de confianza en la entrada HTML, a menos que siga el resto de los pasos que se indican a continuación. Los datos que no son de confianza son cualquier dato que un atacante pueda controlar, como entradas de formulario HTML, cadenas de consulta, encabezados HTTP o incluso datos procedentes de una base de datos, ya que un atacante puede atacar una base de datos sin necesidad de atacar la aplicación.

  2. Antes de colocar datos que no son de confianza dentro de un elemento HTML, asegúrese de que está codificado en HTML. La codificación HTML toma caracteres como < y los cambia a un formato seguro, como <

  3. Antes de colocar datos que no son de confianza en un atributo HTML, asegúrese de que este último está codificado en HTML. La codificación de atributos HTML es un superconjunto de codificación HTML y codifica caracteres adicionales como las comillas.

  4. Antes de colocar datos que no son de confianza en JavaScript, coloque los datos en un elemento HTML cuyo contenido pueda recuperar durante el tiempo de ejecución. Si esto no es posible, asegúrese de que los datos están codificados en JavaScript. La codificación de JavaScript toma caracteres peligrosos para JavaScript y los reemplaza por su hexadecimal, por ejemplo, < se codificaría como \u003C.

  5. Antes de colocar datos que no son de confianza en una cadena de consulta de dirección URL, asegúrese de que está codificada como dirección URL.

Codificación HTML mediante Razor

El motor Razor usado en MVC codifica automáticamente todas las salidas procedentes de variables, a menos que usted tome grandes esfuerzos para evitarlo. Este motor emplea reglas de codificación de atributos HTML siempre que se use la directiva @. Como la codificación de atributos HTML es un superconjunto de la codificación HTML, no tiene que preocuparse por si debe usar la codificación HTML o la codificación de atributos HTML. Debe asegurarse de que solo usa @ en un contexto HTML, no al intentar insertar entradas que no son de confianza directamente en JavaScript. Los asistentes de etiquetas también codificarán la entrada que use en los parámetros de etiqueta.

Eche un vistazo a este código de Razor:

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

@untrustedInput

Esta vista genera el contenido de la variable untrustedInput. Esta variable incluye algunos caracteres que se usan en ataques XSS, es decir <, " y >. Al examinar el origen se muestra la salida representada codificada como:

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

Advertencia

ASP.NET Core MVC proporciona una clase HtmlString que no se codifica automáticamente tras la salida. Esto nunca se debe usar en combinación con la entrada que no es de confianza, ya que expondría una vulnerabilidad XSS.

Codificación de JavaScript mediante Razor

Puede haber ocasiones en las que quiera insertar un valor en JavaScript para procesarlo en la vista. Esto se puede hacer de dos maneras. La manera más segura de insertar valores es colocar el valor en un atributo de datos de una etiqueta y recuperarlo en JavaScript. Por ejemplo:

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

El marcado anterior genera el siguiente código 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>

El código anterior genera la siguiente salida:

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

Advertencia

NO concatene la entrada que no es de confianza en JavaScript para crear elementos DOM ni use document.write() en contenido generado dinámicamente.

Use uno de los métodos siguientes para evitar que el código se exponga a XSS basado en DOM:

  • Use createElement() y asigne valores de propiedad con métodos o propiedades adecuados, como node.textContent= o node.InnerText=.
  • Use document.CreateTextNode() y anéxelo en la ubicación DOM adecuada.
  • element.SetAttribute()
  • element[attribute]=

Acceso a codificadores en el código

Los codificadores HTML, JavaScript y URL están disponibles para el código de dos maneras:

  • Inyéctelos a través de la inserción de dependencias.
  • Use los codificadores predeterminados contenidos en el espacio de nombres System.Text.Encodings.Web.

Si usa los codificadores predeterminados, las personalizaciones aplicadas a los intervalos de caracteres que se tratarán como seguras no surtirán efecto. Los codificadores predeterminados usan las reglas de codificación más seguras posibles.

Para usar los codificadores configurables a través de la inserción de dependencias, los constructores deben tomar un parámetro HtmlEncoder, JavaScriptEncoder o UrlEncoder según corresponda. Por ejemplo:

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 dirección URL de codificación

Si desea crear una cadena de consulta de dirección URL con una entrada que no es de confianza como valor, use UrlEncoder para codificar el valor. Por ejemplo,

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

Después de codificarla, la variable encodedValue se convierte en %22Quoted%20Value%20with%20spaces%20and%20%26%22. Los espacios, las comillas, los signos de puntuación y otros caracteres no seguros se codifican con porcentajes en su valor hexadecimal, por ejemplo, un carácter de espacio se convertirá en %20.

Advertencia

No use una entrada que no sea de confianza como parte de una ruta de acceso URL. Pase siempre una entrada que no sea de confianza como un valor de cadena de consulta.

Personalización de los codificadores

De forma predeterminada, los codificadores usan una lista segura limitada al rango Unicode Basic Latin y codifican todos los caracteres fuera de ese intervalo como sus equivalentes de código de caracteres. Este comportamiento también afecta a la representación de HtmlHelper y Razor TagHelper, ya que usa los codificadores para generar las cadenas.

La idea es proteger contra errores desconocidos o futuros del explorador (los errores del explorador en el pasado han entorpecido el análisis en función del procesamiento de caracteres no inglés). Si su sitio web utiliza caracteres no latinos, como chino, cirílico u otros, probablemente este no es el comportamiento que desea.

Las listas seguras del codificador se pueden personalizar para incluir intervalos Unicode adecuados para la aplicación durante el inicio, en Program.cs:

Por ejemplo, con una configuración predeterminada mediante Razor HtmlHelper similar a esta:

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

El marcado anterior se representa con texto chino codificado:

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

Para ampliar los caracteres tratados como seguros por el codificador, inserte la siguiente línea en Program.cs:

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

Puede personalizar las listas seguras del codificador para incluir intervalos Unicode adecuados para la aplicación durante el inicio, en ConfigureServices().

Por ejemplo, con la configuración predeterminada, podría usar Razor HtmlHelper así;

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

Cuando vea el origen de la página web, comprobará que se ha representado de la siguiente manera con el texto chino codificado;

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

Para ampliar los caracteres tratados como seguros por el codificador, insertaría la siguiente línea en el método ConfigureServices() en startup.cs;

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

En este ejemplo se amplía la lista segura para incluir el rango Unicode CjkUnifiedIdeographs. La salida representada ahora se convertiría en

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

Los intervalos de lista seguros se especifican como gráficos de códigos Unicode, no como idiomas. El estándar Unicode tiene una lista de gráficos de código que puede usar para buscar el gráfico que contiene los caracteres. Cada codificador, Html, JavaScript y Url, debe configurarse por separado.

Nota:

La personalización de la lista segura solo afecta a los codificadores de origen a través de la inserción de dependencias. Si accede directamente a un codificador a través de System.Text.Encodings.Web.*Encoder.Default, entonces se usará solo la lista segura de Basic Latin predeterminada.

¿Dónde debe realizarse la codificación?

La práctica generalmente aceptada es que la codificación tiene lugar en el punto de salida y los valores codificados nunca se deben almacenar en una base de datos. La codificación en el punto de salida permite cambiar el uso de datos, por ejemplo, de HTML a un valor de cadena de consulta. También le permite buscar fácilmente los datos sin tener que codificar valores antes de buscar y aprovechar los cambios o correcciones de errores realizados en los codificadores.

Validación como técnica de prevención de XSS

La validación puede ser una herramienta útil para limitar los ataques XSS. Por ejemplo, una cadena numérica que contiene solo los caracteres 0-9 no desencadenará un ataque XSS. La validación se vuelve más complicada al aceptar HTML en la entrada del usuario. Analizar una entrada HTML es difícil; quizás imposible. El marcado, junto con un analizador que quite el HTML incrustado, es una opción más segura para aceptar entradas enriquecidas. Nunca confíe solo en la validación. Codifique siempre una entrada que no sea de confianza antes de la salida, independientemente de qué validación o saneamiento se hayan realizado.