Crear una experiencia enriquecida en la Web actual casi inevitablemente implica incorporar componentes y contenido sobre los que no tienes un control real. Los widgets de terceros pueden generar participación y desempeñar un papel fundamental en la experiencia general del usuario, y el contenido generado por usuarios a veces es incluso más importante que el contenido nativo de un sitio. Abstenerse de usar cualquiera de ellos no es realmente una opción, pero ambos aumentan el riesgo de que algo malo™ pueda suceder en tu sitio. Cada widget que incorporas (cada anuncio, cada widget de redes sociales) es un posible vector de ataque para quienes tengan intenciones maliciosas:
La Política de Seguridad del Contenido (CSP) puede mitigar los riesgos asociados con ambos tipos de contenido, ya que te brinda la capacidad de incluir en la lista blanca fuentes de secuencias de comandos y otros contenidos específicamente confiables. Este es un paso importante en la dirección correcta, pero vale la pena señalar que la protección que ofrecen la mayoría de las directivas de CSP es binaria: el recurso se permite o no. Hay ocasiones en las que sería útil decir "No estoy seguro de que realmente confío en esta fuente de contenido, pero es muuuy bonita". Insértalo, navegador, pero no permitas que dañe mi sitio".
Privilegio mínimo
En esencia, estamos buscando un mecanismo que nos permita otorgar a contenido que incorporemos solo el nivel mínimo de capacidad necesario para hacer su trabajo. Si un widget no necesita abrir una ventana nueva, quitar el acceso a window.open no es un problema. Si no requiere Flash, desactivar la compatibilidad con complementos no debería ser un problema. Somos lo más seguros posible si seguimos el principio de privilegio mínimo y bloqueamos todas y cada una de las funciones que no son directamente relevantes para la funcionalidad que queremos usar. El resultado es que ya no tenemos que confiar ciegamente en que parte del contenido incorporado no aprovechará los privilegios que no debería usar. Simplemente, no tendrá acceso a la funcionalidad en primer lugar.
Los elementos de iframe
son el primer paso hacia un buen marco de trabajo para una solución de este tipo.
Cargar un componente no confiable en un iframe
proporciona una medida de separación entre tu aplicación y el contenido que deseas cargar. El contenido enmarcado no tendrá acceso al DOM de tu página ni a los datos que almacenaste de forma local, ni podrá dibujar en posiciones arbitrarias de la página. Su alcance se limita al contorno del marco. Sin embargo, la separación no es realmente sólida. La página contenida aún tiene varias opciones para un comportamiento molesto o malicioso: los videos, los complementos y las ventanas emergentes que se reproducen automáticamente son solo la punta del iceberg.
El atributo sandbox
del elemento iframe
nos proporciona exactamente lo que necesitamos para reforzar las restricciones del contenido enmarcado. Podemos indicarle al navegador que cargue el contenido de un marco específico en un entorno de privilegios bajos, lo que permite solo el subconjunto de capacidades necesarias para realizar cualquier tarea que se necesite.
Confía, pero verifica
El botón “Tweet” de Twitter es un excelente ejemplo de una funcionalidad que se puede incorporar de forma más segura en tu sitio a través de una zona de pruebas. Twitter te permite incrustar el botón a través de un iframe con el siguiente código:
<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
style="border: 0; width:130px; height:20px;"></iframe>
Para descubrir qué podemos bloquear, examinemos con cuidado qué capacidades requiere el botón. El código HTML que se carga en el marco ejecuta un fragmento de JavaScript de los servidores de Twitter y genera una ventana emergente con una interfaz de tweet cuando se hace clic en él. Esa interfaz necesita acceso a las cookies de Twitter para vincular el tweet a la cuenta correcta y enviar el formulario de tweet. Eso es todo. El marco no necesita cargar ningún complemento, no necesita navegar por la ventana de nivel superior ni por ninguna otra funcionalidad. Como no los necesita, quitémoslos colocando el contenido del marco en la zona de pruebas.
La zona de pruebas funciona en función de una lista de entidades permitidas. Comenzamos por quitar todos los permisos posibles y, luego, volver a activar las funciones individuales agregando marcas específicas a la configuración de la zona de pruebas. Para el widget de Twitter, decidimos habilitar JavaScript, las ventanas emergentes, el envío de formularios y las cookies de twitter.com. Para hacerlo, podemos agregar un atributo sandbox
a iframe
con el siguiente valor:
<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
src="https://platform.twitter.com/widgets/tweet_button.html"
style="border: 0; width:130px; height:20px;"></iframe>
Eso es todo. Le dimos al fotograma todas las funciones que necesita, y el navegador le denegará el acceso a cualquiera de los privilegios que no le otorgamos explícitamente mediante el valor del atributo sandbox
.
Control detallado de las funciones
Vimos algunas de las posibles marcas de zona de pruebas en el ejemplo anterior. Ahora, analicemos el funcionamiento interno del atributo con más detalle.
Si se proporciona un iframe con un atributo de zona de pruebas vacío, el documento enmarcado estará completamente en la zona de pruebas, lo que lo someterá a las siguientes restricciones:
- JavaScript no se ejecutará en el documento enmarcado. Esto no solo incluye JavaScript cargado de forma explícita a través de etiquetas de secuencia de comandos, sino también controladores de eventos intercalados y URLs de javascript:. Esto también significa que se mostrará el contenido contenido en etiquetas noscript, exactamente como si el usuario hubiera inhabilitado la secuencia de comandos.
- El documento enmarcado se carga en un origen único, lo que significa que fallarán todas las verificaciones de origen coincidente. Los orígenes únicos no coinciden con ningún otro origen, ni siquiera con ellos mismos. Entre otros impactos, esto significa que el documento no tiene acceso a los datos almacenados en las cookies de ningún origen ni a ningún otro mecanismo de almacenamiento (almacenamiento de DOM, base de datos indexada, etc.).
- El documento enmarcado no puede crear ventanas o diálogos nuevos (por ejemplo, a través de
window.open
otarget="_blank"
). - No se pueden enviar formularios.
- No se cargarán los complementos.
- El documento enmarcado solo puede navegar por sí mismo, no a su elemento superior de nivel superior.
Si configuras
window.top.location
, se arrojará una excepción y hacer clic en el vínculo contarget="_top"
no tendrá ningún efecto. - Se bloquean las funciones que se activan automáticamente (elementos de formulario con enfoque automático, videos que se reproducen automáticamente, etcétera).
- No se puede obtener el bloqueo del puntero.
- El atributo
seamless
se ignora en eliframes
que contiene el documento enmarcado.
Esto es muy estricto, y un documento cargado en un iframe
completamente en zona de pruebas representa muy poco riesgo. Por supuesto, tampoco puede hacer mucho de valor: es posible que puedas usar una zona de pruebas completa para cierto contenido estático, pero la mayoría de las veces querrás relajar un poco las restricciones.
A excepción de los complementos, cada una de estas restricciones se puede anular agregando una marca al valor del atributo de la zona de pruebas. Los documentos en zona de pruebas nunca pueden ejecutar complementos, ya que son código nativo sin zona de pruebas, pero todo lo demás está permitido:
allow-forms
permite el envío de formularios.allow-popups
permite (¡sorpresa!) ventanas emergentes.allow-pointer-lock
permite (sorpresa) el bloqueo del puntero.allow-same-origin
permite que el documento mantenga su origen. Las páginas que se carguen desdehttps://example.com/
conservarán el acceso a los datos de ese origen.allow-scripts
permite la ejecución de JavaScript y también permite que las funciones se activen automáticamente (ya que sería trivial implementarlas a través de JavaScript).allow-top-navigation
permite que el documento salga del marco navegando por la ventana de nivel superior.
Con esto en mente, podemos evaluar exactamente por qué terminamos con el conjunto específico de marcas de zona de pruebas en el ejemplo de Twitter anterior:
- Se requiere
allow-scripts
, ya que la página cargada en el marco ejecuta algunos JavaScript para controlar la interacción del usuario. - Se requiere
allow-popups
, ya que el botón muestra un formulario de tweet en una ventana nueva. allow-forms
es obligatorio, ya que se debe enviar el formulario de tweeting.allow-same-origin
es necesario, ya que, de lo contrario, no se podría acceder a las cookies de twitter.com y el usuario no podría acceder para publicar el formulario.
Algo importante que debes tener en cuenta es que las marcas de la zona de pruebas aplicadas a un marco también se aplican a cualquier ventana o marco creado en la zona de pruebas. Esto significa que debemos agregar allow-forms
a la zona de pruebas del marco, aunque el formulario solo exista en la ventana en la que aparece el marco.
Con el atributo sandbox
implementado, el widget obtiene solo los permisos que requiere, y las funciones como los complementos, la navegación superior y el bloqueo del puntero permanecen bloqueadas. Reducción del riesgo de incorporar el widget, sin efectos negativos
Todos los involucrados ganan.
Separación de privilegios
Es evidente que poner en zona de pruebas el contenido de terceros para ejecutar su código no confiable en un entorno con pocos privilegios es beneficioso. Pero, ¿qué sucede con tu código? Confías en ti, ¿no? Entonces, ¿por qué preocuparse por la zona de pruebas?
Te haría la siguiente pregunta: Si tu código no necesita complementos, ¿por qué darle acceso a los complementos? En el mejor de los casos, es un privilegio que nunca usas, y en el peor, es un posible vector para que los atacantes ingresen. El código de todos tiene errores, y prácticamente todas las aplicaciones son vulnerables a la explotación de una forma u otra. Si usas la zona de pruebas para tu propio código, significa que, incluso si un atacante subverte con éxito tu aplicación, no tendrá acceso completo al origen de la aplicación; solo podrá hacer lo que la aplicación podría hacer. Sigue siendo malo, pero no tanto como podría ser.
Puedes reducir aún más el riesgo si divides tu aplicación en fragmentos lógicos y colocas cada fragmento en la zona de pruebas con el privilegio mínimo posible. Esta técnica es muy común en el código nativo: Chrome, por ejemplo, se divide en un proceso de navegador de alto privilegio que tiene acceso al disco duro local y puede establecer conexiones de red, y muchos procesos de renderización de bajo privilegio que realizan el trabajo pesado de analizar contenido no confiable. No es necesario que los procesadores toquen el disco, ya que el navegador se encarga de brindarles toda la información que necesitan para renderizar una página. Incluso si un hacker inteligente encuentra una forma de dañar un renderizador, no llegará muy lejos, ya que el renderizador no puede hacer mucho por sí solo: todo el acceso de alto privilegio debe enrutarse a través del proceso del navegador. Los atacantes deberán encontrar varios agujeros en diferentes partes del sistema para causar daños, lo que reduce en gran medida el riesgo de que se lleve a cabo un ataque exitoso.
Cómo usar la zona de pruebas de eval()
de forma segura
Con la zona de pruebas y la
API de postMessage
, el
éxito de este modelo es bastante sencillo de aplicar a la Web. Algunas partes de tu aplicación pueden alojarse en iframe
de zona de pruebas, y el documento superior puede intervenir la comunicación entre ellas mediante la publicación de mensajes y la escucha de respuestas. Este tipo de estructura garantiza que los exploits en cualquier parte de la
app hagan el daño mínimo posible. También tiene la ventaja de obligarte a crear puntos de integración claros, de modo que sepas exactamente dónde debes tener cuidado con la validación de entradas y salidas. Veamos un ejemplo de juguete para ver cómo podría funcionar.
Evalbox es una aplicación emocionante que toma una cadena y la evalúa como JavaScript. Guau, ¿verdad? Justo lo que estabas esperando todos estos años. Por supuesto, es una aplicación bastante peligrosa, ya que permitir la ejecución arbitraria de JavaScript significa que todos los datos que ofrece un origen están disponibles. Mitigaremos el riesgo de que ocurran situaciones negativas asegurándonos de que el código se ejecute dentro de una zona de pruebas, lo que lo hace mucho más seguro. Analizaremos el código de adentro hacia afuera, comenzando con el contenido del marco:
<!-- frame.html -->
<!DOCTYPE html>
<html>
<head>
<title>Evalbox's Frame</title>
<script>
window.addEventListener('message', function (e) {
var mainWindow = e.source;
var result = '';
try {
result = eval(e.data);
} catch (e) {
result = 'eval() threw an exception.';
}
mainWindow.postMessage(result, event.origin);
});
</script>
</head>
</html>
Dentro del marco, tenemos un documento mínimo que simplemente escucha los mensajes de su elemento superior conectando el evento message
del objeto window
.
Cada vez que el elemento superior ejecute postMessage en el contenido del iframe, se activará este evento, lo que nos dará acceso a la string que nuestro superior desea que ejecutemos.
En el controlador, tomamos el atributo source
del evento, que es la ventana superior. La usaremos para enviar el resultado de nuestro arduo trabajo cuando terminemos. Luego, haremos el trabajo pesado. Para ello, pasaremos los datos proporcionados a eval()
. Esta llamada se unió a un bloque de prueba, ya que las operaciones prohibidas dentro de un iframe
de zona de pruebas suelen generar excepciones del DOM. En su lugar, las detectaremos y, en su lugar, informaremos un mensaje de error. Por último, publicamos el resultado
en la ventana superior. Esto es bastante sencillo.
El elemento superior es igualmente sencillo. Crearemos una IU pequeña con un textarea
para el código y un button
para la ejecución, y extraeremos frame.html
a través de un iframe
en zona de pruebas, lo que solo permitirá la ejecución de secuencias de comandos:
<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
id='sandboxed'
src='frame.html'></iframe>
Ahora conectaremos todo para la ejecución. Primero, escucharemos las respuestas de iframe
y las alert()
a nuestros usuarios. Se supone que una aplicación real haría algo menos molesto:
window.addEventListener('message',
function (e) {
// Sandboxed iframes which lack the 'allow-same-origin'
// header have "null" rather than a valid origin. This means you still
// have to be careful about accepting data via the messaging API you
// create. Check that source, and validate those inputs!
var frame = document.getElementById('sandboxed');
if (e.origin === "null" && e.source === frame.contentWindow)
alert('Result: ' + e.data);
});
A continuación, conectaremos un controlador de eventos a los clics en button
. Cuando el usuario haga clic, tomaremos el contenido actual de textarea
y lo pasaremos al marco para su ejecución:
function evaluate() {
var frame = document.getElementById('sandboxed');
var code = document.getElementById('code').value;
// Note that we're sending the message to "*", rather than some specific
// origin. Sandboxed iframes which lack the 'allow-same-origin' header
// don't have an origin which you can target: you'll have to send to any
// origin, which might alow some esoteric attacks. Validate your output!
frame.contentWindow.postMessage(code, '*');
}
document.getElementById('safe').addEventListener('click', evaluate);
Fácil, ¿verdad? Creamos una API de evaluación muy simple y podemos asegurarnos de que el código que se evalúa no tenga acceso a información sensible, como cookies o almacenamiento de DOM. Del mismo modo, el código evaluado no puede cargar complementos, abrir ventanas nuevas ni realizar otras actividades molestas o maliciosas.
Puedes hacer lo mismo con tu propio código si divides las aplicaciones monolíticas en componentes de un solo propósito. Cada una puede unirse a una API de mensajería simple, como lo que escribimos antes. La ventana superior de alto privilegio puede actuar como controlador y despachador, enviar mensajes a módulos específicos que tienen los menos privilegios posibles para realizar sus tareas, escuchar los resultados y garantizar que cada módulo esté bien alimentado con solo la información que requiere.
Sin embargo, ten en cuenta que debes tener mucho cuidado cuando se trata de contenido enmarcado que proviene del mismo origen que el elemento superior. Si una página en https://example.com/
enmarca otra página en el mismo origen con una zona de pruebas que incluye las marcas allow-same-origin y allow-scripts, la página enmarcada puede llegar hasta el elemento superior y quitar el atributo de zona de pruebas por completo.
Juega en tu zona de pruebas
La zona de pruebas está disponible en varios navegadores: Firefox 17 y versiones posteriores, IE 10 y versiones posteriores, y Chrome en el momento de escribir este artículo (por supuesto, caniuse tiene una tabla de compatibilidad actualizada). Aplicar el atributo sandbox
al iframes
que incluyes te permite otorgar ciertos privilegios al contenido que muestran, solo los privilegios necesarios para que el contenido funcione correctamente. Esto te permite reducir el riesgo asociado con la inclusión de contenido de terceros, más allá de lo que ya es posible gracias a la Política de Seguridad del Contenido.
Además, el entorno de pruebas es una técnica eficaz para reducir el riesgo de que un atacante inteligente pueda aprovechar los errores de tu propio código. Mediante la separación de una aplicación monolítica en un conjunto de servicios de zona de pruebas, cada uno de los cuales es responsable de una pequeña parte de la funcionalidad independiente, los atacantes se verán obligados no solo a comprometer el contenido de marcos específicos, sino también su controlador. Esa es una tarea mucho más difícil, especialmente porque se puede reducir mucho el alcance del controlador. Puedes dedicar tu esfuerzo relacionado con la seguridad a auditar ese código si le pides ayuda al navegador con el resto.
Eso no quiere decir que el entorno de pruebas es una solución completa al problema de seguridad en Internet. Ofrece una defensa en profundidad y, a menos que tengas control sobre los clientes de tus usuarios, aún no puedes contar con la compatibilidad del navegador para todos tus usuarios (si controlas a los clientes de tus usuarios, un entorno empresarial, por ejemplo, ¡hurra!). Algún día… pero, por ahora, la zona de pruebas es otra capa de protección para fortalecer tus defensas, no es una defensa completa en la que puedas confiar únicamente. Aun así, las capas son excelentes. Te sugiero que uses esta.
Lecturas adicionales
“Privilege Separation in HTML5 Applications” es un artículo interesante que funciona a través del diseño de un pequeño framework y su aplicación a tres apps de HTML5 existentes.
La zona de pruebas puede ser aún más flexible cuando se combina con otros dos atributos de iframe nuevos:
srcdoc
yseamless
. El primero te permite propagar contenido con contenido sin la sobrecarga de una solicitud HTTP, y el segundo permite que el estilo fluya al contenido enmarcado. Por el momento, ambos tienen una compatibilidad bastante pobre con los navegadores (compilaciones nocturnas de Chrome y WebKit), pero será una combinación interesante en el futuro. Por ejemplo, puedes usar la zona de pruebas para los comentarios de un artículo con el siguiente código:<iframe sandbox seamless srcdoc="<p>This is a user's comment! It can't execute script! Hooray for safety!</p>"></iframe>