Cómo implementar un CSP basado en nonces de scripts o hashes como defensa en profundidad contra el scripting entre sitios.
¿Por qué deberías implementar una política de seguridad de contenido (CSP) estricta?
Cross-site scripting (XSS) o secuencias de comandos en sitios cruzados, definida como la capacidad de inyectar scripts maliciosos en una aplicación web, ha sido una de las mayores vulnerabilidades de seguridad web durante más de una década.
La Política de seguridad de contenido (CSP) es una capa adicional de seguridad que ayuda a mitigar XSS. La configuración de un CSP implica agregar el encabezado HTTP Content-Security-Policy a una página web y establecer valores para controlar qué recursos puede cargar el agente de usuario para esa página. Este artículo explica cómo usar un CSP basado en nonces o hashes para mitigar XSS en lugar de los CSP basados en listas de permisos de host de uso común que a menudo dejan la página expuesta a XSS, ya que se pueden omitir en la mayoría de las configuraciones.
Una política de seguridad de contenido basada en nonces o hashes a menudo se denomina CSP estricto . Cuando una aplicación usa un CSP estricto, los atacantes que encuentran fallas en la inyección de HTML generalmente no podrán usarlos para forzar al navegador a ejecutar scripts maliciosos en el contexto del documento vulnerable. Esto se debe a que el CSP estricto solo permite scripts hash o scripts con el valor de nonce correcto generado en el servidor, por lo que los atacantes no pueden ejecutar el script sin conocer el nonce correcto para una respuesta determinada.
Por qué se recomienda un CSP estricto en lugar de los CSP de lista de permitidos
Si tu sitio ya tiene un CSP con este aspecto: script-src www.googleapis.com
, ¡es posible que no sea eficaz contra las secuencias de comandos entre sitios (cross-site scripting)! Este tipo de CSP se denomina CSP de lista de permitidos y tiene un par de desventajas:
- Requiere mucha personalización.
- Se puede omitir en la mayoría de las configuraciones .
Esto hace que los CSP de listas de permisos sean generalmente ineficaces para evitar que los atacantes exploten XSS. Es por eso que se recomienda utilizar un CSP estricto basado en nonces o hashes criptográficos, lo que evita las trampas descritas anteriormente.
: no protege eficazmente su sitio. ❌ - Debe ser altamente personalizado. 😓
- Protege eficazmente su sitio. ✅
- Siempre tiene la misma estructura. 😌
¿Qué es una política de seguridad de contenido estricta?
Una política de seguridad de contenido estricta tiene la siguiente estructura y se habilita configurando uno de los siguientes encabezados de respuesta HTTP:
- CSP estricto basado en nonce
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
- CSP estricto basado en hash
Content-Security-Policy:
script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
Las siguientes propiedades hacen que un CSP como el anterior sea "estricto" y, por lo tanto, seguro:
Utiliza nonces
'nonce-{RANDOM}'
o hashes'sha256-{HASHED_INLINE_SCRIPT}'
para indicar qué<script>
son confiables para el desarrollador del sitio y deben poder ejecutarse en el navegador del usuario.Establece
'strict-dynamic'
para reducir el esfuerzo de implementar un CSP basado en hash o nonce al permitir automáticamente la ejecución de scripts creados por un script que ya es de confianza. Esto también desbloquea el uso de la mayoría de las bibliotecas y widgets de JavaScript de terceros.No se basa en listas de URL permitidas y, por lo tanto, no sufre omisiones de CSP comunes.
Bloquea scripts en línea que no son de confianza, como controladores de eventos en línea o
javascript:
URIs.Restringe
object-src
para deshabilitar complementos peligrosos como Flash.Restringe la
base-uri
para bloquear la inyección de etiquetas<base>
Esto evita que los atacantes cambien la ubicación de los scripts cargados desde URL relativas.
Adopción de un CSP estricto
Para adoptar un CSP estricto, debes:
- Decidir si tu aplicación debe establecer un CSP basado en hash o nonce.
- Copia el CSP de la sección ¿Qué es una política de seguridad de contenido estricta? Y configúralo como un encabezado de respuesta en tu aplicación.
- Refactorizar las plantillas HTML y el código del lado del cliente para eliminar patrones que sean incompatibles con CSP.
- Agregar alternativas para admitir Safari y navegadores más antiguos.
- Implementar tu CSP.
Puedes utilizar la auditoría de prácticas recomendadas Lighthouse (v7.3.0 y superior con flag --preset=experimental
) a lo largo de este proceso para comprobar si tu sitio tiene un CSP y si es lo suficientemente estricto como para ser eficaz contra XSS.
Paso 1: Decide si necesitas un CSP basado en hash o en nonce
Hay dos tipos de CSP estrictos, basados en hash y basados en nonce. Así es como funcionan:
- CSP basado en Nonce: generas un número aleatorio en tiempo de ejecución, lo incluyes en tu CSP y lo asocias con cada etiqueta de secuencia de comandos en tu página. Un atacante no puede incluir y ejecutar un script malicioso en su página, porque necesitaría adivinar el número aleatorio correcto para ese script. Esto solo funciona si el número no se puede adivinar y se genera nuevamente en tiempo de ejecución para cada respuesta.
- CSP basado en hash: el hash de cada etiqueta de secuencia de comandos en línea se agrega al CSP. Ten en cuenta que cada secuencia de comandos tiene un hash diferente. Un atacante no puede incluir y ejecutar un script malicioso en tu página, porque el hash de ese script debería estar presente en tu CSP.
Criterios para elegir un enfoque de CSP estricto:
CSP basado en Nonce | Para páginas HTML renderizadas en el servidor donde puedes crear un nuevo token aleatorio (nonce) para cada respuesta. |
---|---|
CSP basado en hash | Para páginas HTML servidas estáticamente o aquellas que necesitan ser almacenadas en caché. Por ejemplo, aplicaciones web de una sola página creadas con marcos como Angular, React u otros, que se sirven estáticamente sin renderizado del lado del servidor. |
Paso 2: Establece un CSP estricto y prepara tus scripts
Al configurar un CSP, tienes algunas opciones:
- Modo de solo informe (
Content-Security-Policy-Report-Only
) o modo de aplicación (Content-Security-Policy
). En el modo de solo informe, el CSP no bloqueará los recursos todavía, nada se romperá, pero podrá ver errores y recibir informes de lo que se habría bloqueado. Localmente, cuando estás en el proceso de configurar un CSP, esto realmente no importa, porque ambos modos te mostrarán los errores en la consola del navegador. En todo caso, el modo de aplicación te facilitará aún más ver los recursos bloqueados y modificar tu CSP, ya que tu página se verá rota. El modo de solo informe se vuelve más útil más adelante en el proceso (consulte el Paso 5 ). - Encabezado o etiqueta HTML
<meta>
. Para el desarrollo local, una<meta>
puede ser más conveniente para ajustar tu CSP y ver rápidamente cómo afecta tu sitio. Sin embargo:- Más adelante, al implementar tu CSP en producción, se recomienda configurarlo como un encabezado HTTP.
- Si deseas configurar tu CSP en modo de solo informe, deberás configurarlo como un encabezado; las metaetiquetas de CSP no admiten el modo de solo informe.
Establece el siguiente encabezado de respuesta HTTP Content-Security-Policy
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
Generar un nonce para CSP
Un nonce es un número aleatorio que se usa solo una vez por carga de página. Un CSP basado en nonce solo puede mitigar XSS si un atacante no puede adivinar el valor de nonce. Un nonce para CSP debe ser:
- Un valor aleatorio criptográficamente fuerte (idealmente 128+ bits de longitud)
- Recién generado para cada respuesta
- Codificado en Base64
A continuación, se muestran algunos ejemplos sobre cómo agregar un nonce CSP en marcos del lado del servidor:
- Django (pitón)
- Express (JavaScript):
const app = express();
app.get('/', function(request, response) {
// Generate a new random nonce value for every response.
const nonce = crypto.randomBytes(16).toString("base64");
// Set the strict nonce-based CSP response header
const csp =script-src 'nonce-${nonce}' 'strict-dynamic' https:; object-src 'none'; base-uri 'none';
;
response.set("Content-Security-Policy", csp);
// Every <script> tag in your application should set thenonce
attribute to this value.
response.render(template, { nonce: nonce });
});
}
Agregar un nonce
a los elementos <script>
Con un CSP basado en nonce, cada <script>
debe tener un nonce
que coincida con el valor nonce aleatorio especificado en el encabezado del CSP (todos los scripts pueden tener el mismo nonce). El primer paso es agregar estos atributos a todos los scripts:
<script src="/path/to/script.js"></script>
<script>foo()</script>
Establece el siguiente encabezado de respuesta HTTP Content-Security-Policy
Content-Security-Policy:
script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
Para varios scripts en línea, la sintaxis es la siguiente: 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'
.
Cargar scripts de origen dinámicamente
Todas las secuencias de comandos de origen externo deben cargarse dinámicamente a través de una secuencia de comandos en línea, ya que los hashes CSP sólo son compatibles con los navegadores para las secuencias de comandos en línea (los hashes para las secuencias de comandos de origen no son compatibles con los navegadores).
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
<script>
var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];
scripts.forEach(function(scriptUrl) {
var s = document.createElement('script');
s.src = scriptUrl;
s.async = false; // to preserve execution order
document.head.appendChild(s);
});
</script>
En el fragmento de código anterior, se añade s.async = false
para garantizar que foo se ejecute antes que bar (incluso si bar se carga primero). En este fragmento, s.async = false
no bloquea el analizador mientras se cargan los scripts; eso es porque los scripts se agregan dinámicamente. El analizador solo se detendrá mientras se ejecutan los scripts, tal como se comportaría con los scripts async
. Sin embargo, con este fragmento, ten en cuenta:
- Uno o ambos scripts pueden ejecutarse antes de que el documento haya terminado de descargarse. Si deseas que el documento esté listo para cuando se ejecuten los scripts, debes esperar al evento
DOMContentLoaded
antes de agregar los scripts. Si esto causa un problema de rendimiento (porque los scripts no comienzan a descargarse lo suficientemente temprano), puedes usar etiquetas de precarga anteriormente en la página. defer = true
no hará nada. Si necesitas ese comportamiento, tendrás que ejecutar manualmente el script en el momento en que desees ejecutarlo.
Paso 3: Refactoriza las plantillas HTML y el código del lado del cliente para eliminar patrones incompatibles con CSP
Los controladores de eventos en línea (como onclick="…"
, onerror="…"
) y los URI de JavaScript ( <a href="javascript:…">
) se pueden utilizar para ejecutar scripts. Esto significa que un atacante que encuentre un error XSS podría inyectar este tipo de HTML y ejecutar JavaScript malicioso. Un CSP basado en hash o nonce no permite el uso de dicho marcado. Si tu sitio utiliza alguno de los patrones descritos anteriormente, deberás refactorizarlos en alternativas más seguras.
Si habilitaste CSP en el paso anterior, podrás ver las infracciones de CSP en la consola cada vez que CSP bloquee un patrón incompatible.
En la mayoría de los casos, la solución es sencilla:
Para refactorizar los controladores de eventos en línea, vuelve a escribirlos para agregarlos desde un bloque de JavaScript
<span onclick="doThings();">A thing.</span>
<span id="things">A thing.</span>
<script nonce="${nonce}">
document.getElementById('things')
.addEventListener('click', doThings);
</script>
Para javascript:
URIs, puedes usar un patrón similar
<a href="javascript:linkClicked()">foo</a>
<a id="foo">foo</a>
<script nonce="${nonce}">
document.getElementById('foo')
.addEventListener('click', linkClicked);
</script>
Uso de eval()
en JavaScript
Si tu aplicación usa eval()
para convertir las serializaciones de cadenas JSON en objetos JS, debes refactorizar dichas instancias a JSON.parse()
, que también es más rápido.
Si no puede eliminar todos los usos de eval()
, aún puedes establecer un CSP estricto basado en nonce, pero tendrás que usar 'unsafe-eval'
que hará que tu política sea un poco menos segura.
Puedes encontrar estos y más ejemplos de refactorización de este tipo en este estricto CSP Codelab:
Paso 4: Agrega alternativas para admitir Safari y navegadores más antiguos
CSP es compatible con todos los navegadores principales, pero necesitará dos alternativas:
El uso de
'strict-dynamic'
requiere agregarhttps:
como respaldo para Safari, el único navegador importante sin soporte para'strict-dynamic'
. Al hacerlo:- Todos los navegadores que admiten
'strict-dynamic'
ignoraránhttps:
fallback, por lo que esto no reducirá la solidez de la política. - En Safari, los scripts de fuentes externas solo podrán cargarse si provienen de un origen HTTPS. Esto es menos seguro que un CSP estricto, es una alternativa, pero aún evitaría ciertas causas comunes de XSS como inyecciones de
javascript:
URIs porque'unsafe-inline'
no está presente o se ignora en presencia de un hash o un nonce.
- Todos los navegadores que admiten
Para garantizar la compatibilidad con versiones de navegador muy antiguas (más de 4 años), puedes agregar
'unsafe-inline'
como respaldo. Todos los navegadores recientes ignorarán'unsafe-inline'
si hay un código de acceso o un hash de CSP.
Content-Security-Policy:
script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
Paso 5: Implementa tu CSP
Después de confirmar que CSP no está bloqueando scripts legítimos en tu entorno de desarrollo local, puedes continuar con la implementación de tu CSP en su (puesta en escena, luego) entorno de producción:
- (Opcional) Implementa tu CSP en modo de solo informe mediante el
Content-Security-Policy-Report-Only
. Puedes obtener más información sobre la API de informes. El modo de solo informe es útil para probar un cambio potencialmente importante como un nuevo CSP en producción, antes de aplicar las restricciones de CSP. En el modo de solo informe, tu CSP no afecta el comportamiento de tu aplicación (nada realmente se romperá). Pero el navegador seguirá generando errores de consola e informes de infracción cuando se encuentren patrones incompatibles con CSP (para que pueda ver lo que habría fallado para sus usuarios finales). - Una vez que estés seguro de que tu CSP no provocará daños para tus usuarios finales, implementa tu CSP utilizando el encabezado de respuesta
Content-Security-Policy
Solo una vez que hayas completado este paso, CSP comenzará a proteger tu aplicación de XSS. Configurar tu CSP a través de un encabezado HTTP del lado del servidor es más seguro que configurarlo como una etiqueta<meta>
; usa un encabezado si puedes.
Limitaciones
En términos generales, un CSP estricto proporciona una fuerte capa adicional de seguridad que ayuda a mitigar XSS. En la mayoría de los casos, CSP reduce significativamente la superficie de ataque (patrones peligrosos como javascript:
URIs están completamente desactivados). Sin embargo, según el tipo de CSP que estés utilizando (nonces, hashes, con o sin 'strict-dynamic'
), hay casos en los que CSP no protege:
- Si ingresas un script, pero hay una inyección directamente en el cuerpo o en el
src
de ese elemento<script>
. - Si hay inyecciones en las ubicaciones de scripts creados dinámicamente (
document.createElement('script')
), incluso en cualquier función de biblioteca que creescript
basados en el valor de sus argumentos. Esto incluye algunas API comunes como.html()
jQuery, así como.get()
y.post()
en jQuery <3.0. - Si hay inyecciones de plantilla en aplicaciones antiguas de AngularJS. Un atacante que pueda inyectar una plantilla AngularJS puede usarla para ejecutar JavaScript arbitrario.
- Si la política contiene
'unsafe-eval'
, inyecciones eneval()
,setTimeout()
y algunas otras API de uso poco frecuente.
Los desarrolladores y los ingenieros de seguridad deben prestar especial atención a estos patrones durante las revisiones de código y las auditorías de seguridad. Puedes encontrar más detalles sobre los casos descritos anteriormente en esta presentación de CSP.
Otras lecturas
- La CSP ha muerto, ¡viva la CSP! Sobre la inseguridad de las listas blancas y el futuro de la política de seguridad del contenido
- Evaluador de CSP
- Conferencia LocoMoco: Política de seguridad de contenido: un lío exitoso entre el endurecimiento y la mitigación
- Charla de I/O de Google: protección de aplicaciones web con funciones de plataforma modernas