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 ciberdelincuente colocar scripts en el lado cliente (normalmente a través de JavaScript) en páginas web. Los scripts del ciberdelincuente se ejecutan cuando otros usuarios cargan páginas afectadas, lo que permite al ciberdelincuente 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 explorador 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 deposita 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 ciberdelincuente 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:
Nunca coloques datos que no sean de confianza en la entrada HTML, a menos que sigas el rest de los pasos que se indican a continuación. Los datos que no son de confianza son cualquier dato que un ciberdelincuente pueda controlar, como entradas de formulario HTML, cadenas de consulta, encabezados HTTP o incluso datos con origen en una base de datos, ya que un ciberdelincuente puede breach tu base de datos sin necesidad de breachtu aplicación.
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 <
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.
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
.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:
<"123">
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="<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>
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, comonode.textContent=
onode.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="/">汉语/漢語</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="/">汉语/漢語</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.