En esta sección, describiremos algunos principios generales para prevenir vulnerabilidades de scripting entre sitios y formas de usar varias tecnologías comunes para protegerse contra ataques XSS.
La prevención de scripts entre sitios generalmente se puede lograr a través de dos capas de defensa:
- Codificar datos en la salida
- Validar la entrada a la llegada
Puede utilizar Burp Scanner para escanear sus sitios web en busca de numerosas vulnerabilidades de seguridad, incluido XSS. La lógica de escaneo de vanguardia de Burp replica las acciones de un atacante experto y es capaz de lograr una cobertura correspondientemente alta de vulnerabilidades XSS. Puede utilizar el escáner de eructos para asegurarse de que sus defensas contra los ataques XSS funcionan de manera efectiva.
Codificar datos en la salida
La codificación debe aplicarse directamente antes de escribir datos controlables por el usuario en una página, porque el contexto en el que está escribiendo determina el tipo de codificación que necesita usar. Por ejemplo, los valores dentro de una cadena de JavaScript requieren un tipo de escape diferente a los de un contexto HTML.
En un contexto HTML, debe convertir valores no incluidos en la lista blanca en entidades HTML:
-
<
convierte a:<
-
>
convierte a:>
En un contexto de cadena de JavaScript, los valores no alfanuméricos deben tener escape Unicode:
-
<
convierte a:\u003c
-
>
convierte a:\u003e
A veces, tendrá que aplicar varias capas de codificación, en el orden correcto. Por ejemplo, para incrustar de forma segura la entrada del usuario dentro de un controlador de eventos, debe trabajar tanto con el contexto JavaScript como con el contexto HTML. Por lo tanto, primero debe escapar Unicode de la entrada y luego codificarla en HTML:
<a href="#" onclick="x='This string needs two layers of escaping'">test</a>
Validar entrada a la llegada
La codificación es probablemente la línea de defensa XSS más importante, pero no es suficiente para evitar vulnerabilidades XSS en todos los contextos. También debe validar la entrada lo más estrictamente posible en el momento en que se recibe por primera vez de un usuario.
Los ejemplos de validación de entrada incluyen:
- Si un usuario envía una URL que se devolverá en respuestas, se validará que comienza con un protocolo seguro como HTTP y HTTPS. De lo contrario, alguien podría explotar su sitio con un protocolo dañino como
javascript
odata
. - Si un usuario proporciona un valor que esperaba que fuera numérico, validando que el valor realmente contiene un entero.
- Validar esa entrada contiene solo un conjunto de caracteres esperado.
La validación de entrada debería funcionar idealmente bloqueando la entrada no válida. Un enfoque alternativo, de intentar limpiar la entrada no válida para hacerla válida, es más propenso a errores y debe evitarse siempre que sea posible.
Lista blanca vs lista negra
La validación de entrada generalmente debe emplear listas blancas en lugar de listas negras. Por ejemplo, en lugar de intentar hacer una lista de todos los protocolos dañinos (javascript
, data
, etc.), simplemente haga una lista de protocolos seguros (HTTP, HTTPS) y no permita nada que no esté en la lista. Esto asegurará que su defensa no se rompa cuando aparezcan nuevos protocolos dañinos y la hará menos susceptible a ataques que buscan ofuscar valores no válidos para evadir una lista negra.
Permitir HTML «seguro»
Permitir a los usuarios publicar marcado HTML debe evitarse siempre que sea posible, pero a veces es un requisito comercial. Por ejemplo, un sitio de blog podría permitir que se publiquen comentarios que contengan un marcado HTML limitado.
El enfoque clásico es intentar filtrar etiquetas y JavaScript potencialmente dañinos. Puede intentar implementar esto utilizando una lista blanca de etiquetas y atributos seguros, pero gracias a las discrepancias en los motores de análisis del navegador y peculiaridades como mutation XSS, este enfoque es extremadamente difícil de implementar de forma segura.
La opción menos mala es usar una biblioteca JavaScript que realice filtrado y codificación en el navegador del usuario, como DOMPurify. Otras bibliotecas permiten a los usuarios proporcionar contenido en formato markdown y convertir el markdown en HTML. Desafortunadamente, todas estas bibliotecas tienen vulnerabilidades XSS de vez en cuando, por lo que esta no es una solución perfecta. Si usa uno, debe vigilar de cerca las actualizaciones de seguridad.
Nota
Además de JavaScript, otros contenidos como CSS e incluso HTML normal pueden ser perjudiciales en algunas situaciones.
Ataques con CSS malicioso
Cómo evitar que XSS utilice un motor de plantillas
Muchos sitios web modernos utilizan motores de plantillas del lado del servidor, como Twig y Freemarker, para incrustar contenido dinámico en HTML. Estos típicamente definen su propio sistema de escape. Por ejemplo, en Twig, puede usar el filtro e()
, con un argumento que defina el contexto:
{{ user.firstname | e('html') }}
Algunos otros motores de plantillas, como Jinja y React, escapan al contenido dinámico de forma predeterminada, lo que evita de manera efectiva la mayoría de las ocurrencias de XSS.
Recomendamos revisar las características de escape de cerca cuando evalúe si se debe usar un motor de plantilla o marco de trabajo determinado.
Nota
Si concatena directamente la entrada del usuario en cadenas de plantilla, será vulnerable a la inyección de plantilla del lado del servidor, que a menudo es más grave que XSS.
Cómo prevenir XSS en PHP
En PHP hay una función incorporada para codificar entidades llamada htmlentities
. Debe llamar a esta función para escapar de su entrada cuando esté dentro de un contexto HTML. La función debe ser llamada con tres argumentos:
- Tu cadena de entrada.
-
ENT_QUOTES
, que es un indicador que especifica que todas las comillas deben codificarse. - El conjunto de caracteres, que en la mayoría de los casos debe ser UTF-8.
Por ejemplo:
<?php echo htmlentities($input, ENT_QUOTES, 'UTF-8');?>
Cuando está en un contexto de cadena de JavaScript, necesita una entrada de escape Unicode como ya se ha descrito. Desafortunadamente, PHP no proporciona una API para que Unicode escape una cadena. Aquí está el código para hacerlo en PHP:
<?php
function jsEscape($str) {
$output = '';
$str = str_split($str);
for($i=0;$i<count($str);$i++) {
$chrNum = ord($str);
$chr = $str;
if($chrNum === 226) {
if(isset($str) && ord($str) === 128) {
if(isset($str) && ord($str) === 168) {
$output .= '\u2028';
$i += 2;
continue;
}
if(isset($str) && ord($str) === 169) {
$output .= '\u2029';
$i += 2;
continue;
}
}
}
switch($chr) {
case "'":
case '"':
case "\n";
case "\r";
case "&";
case "\";
case "<":
case ">":
$output .= sprintf("\u%04x", $chrNum);
break;
default:
$output .= $str;
break;
}
}
return $output;
}
?>
Aquí es cómo utilizar la jsEscape
función en PHP:
<script>x = '<?php echo jsEscape($_GET)?>';</script>
Alternativamente, usted podría usar un motor de plantillas.
Cómo evitar el lado del cliente XSS en JavaScript
Para escapar de la entrada del usuario en un contexto HTML en JavaScript, necesita su propio codificador HTML porque JavaScript no proporciona una API para codificar HTML. Aquí hay un ejemplo de código JavaScript que convierte una cadena en entidades HTML:
function htmlEncode(str){
return String(str).replace(//gi, function(c){
return '&#'+c.charCodeAt(0)+';';
});
}
A continuación, utilizaría esta función de la siguiente manera:
<script>document.body.innerHTML = htmlEncode(untrustedValue)</script>
Si la entrada está dentro de una cadena JavaScript, necesita un codificador que realice el escape Unicode. Aquí hay un codificador Unicode de muestra:
function jsEscape(str){
return String(str).replace(//gi, function(c){
return '\u'+('0000'+c.charCodeAt(0).toString(16)).slice(-4);
});
}
A continuación, utilizaría esta función de la siguiente manera:
<script>document.write('<script>x="'+jsEscape(untrustedValue)+'";<\/script>')</script>
Cómo evitar XSS en jQuery
La forma más común de XSS en jQuery es cuando pasa la entrada del usuario a un selector de jQuery. Los desarrolladores web a menudo usarían location.hash
y lo pasarían al selector, lo que causaría XSS, ya que jQuery renderizaría el HTML. jQuery reconoció este problema y parcheó su lógica de selector para comprobar si la entrada comienza con un hash. Ahora jQuery solo renderizará HTML si el primer carácter es <
. Si pasa datos que no son de confianza al selector de jQuery, asegúrese de escapar correctamente del valor con la función jsEscape
anterior.
Mitigar XSS mediante la política de seguridad de contenido (CSP)
La política de seguridad de contenido (CSP) es la última línea de defensa contra la creación de scripts entre sitios. Si la prevención de XSS falla, puede usar CSP para mitigar XSS restringiendo lo que puede hacer un atacante.
CSP le permite controlar varias cosas, como si se pueden cargar scripts externos y si se ejecutarán scripts en línea. Para implementar CSP, debe incluir un encabezado de respuesta HTTP llamado Content-Security-Policy
con un valor que contenga su directiva.
Un ejemplo de CSP es el siguiente:
default-src 'self'; script-src 'self'; object-src 'none'; frame-src 'none'; base-uri 'none';
Esta directiva especifica que los recursos, como imágenes y scripts, solo se pueden cargar desde el mismo origen que la página principal. Por lo tanto, incluso si un atacante puede inyectar con éxito una carga útil XSS, solo puede cargar recursos del origen actual. Esto reduce en gran medida la posibilidad de que un atacante pueda explotar la vulnerabilidad XSS.
Si necesita cargar recursos externos, asegúrese de permitir solo scripts que no ayuden a un atacante a explotar su sitio. Por ejemplo, si incluyes ciertos dominios en la lista blanca, un atacante puede cargar cualquier script de esos dominios. Siempre que sea posible, intente alojar recursos en su propio dominio.
Si eso no es posible, puede usar una política basada en hash o nonce para permitir scripts en diferentes dominios. Un nonce es una cadena aleatoria que se agrega como atributo de un script o recurso, que solo se ejecutará si la cadena aleatoria coincide con la generada por el servidor. Un atacante no puede adivinar la cadena aleatoria y, por lo tanto, no puede invocar un script o recurso con un nonce válido, por lo que el recurso no se ejecutará.