Los desarrolladores no entienden CORS (2019)
(fosterelli.co)- La vulnerabilidad del servidor web local de Zoom mostró que, cuando muchos desarrolladores web entienden mal cómo funciona CORS, los límites de seguridad pueden romperse con facilidad
- Zoom se comunicaba con el servidor local en
localhost:19421y, en lugar de usar AJAX, transmitía códigos de estado mediante el tamaño de una imagen, lo que puede interpretarse como una implementación de evasión para esquivar CORS - Chrome aplica headers de CORS también a los servidores web en localhost, y la comunicación entre frontend y backend en distintos puertos de localhost también está soportada por el navegador
- Un diseño más seguro sería que el servidor local ofreciera una API REST y configurara
Access-Control-Allow-Originpara restringir el acceso solo al JavaScript de zoom.us - Saltarse la política del mismo origen puede hacer que el código funcione, pero también puede exponer a todos los sitios web de internet funciones privilegiadas del servidor local
La evasión de CORS creada por el servidor web local de Zoom
- Al trabajar en consultoría full-stack con desarrolladores de distintos tamaños de empresa e industrias, se observó repetidamente que muchos desarrolladores web no entienden bien CORS
- En la vulnerabilidad reciente de Zoom, el investigador de seguridad Jonathan Leitschuh descubrió que Zoom levantaba un servidor web en
http://localhost:19421en la máquina del usuario- Cuando el usuario abre un enlace de Zoom, el sitio web de Zoom envía una solicitud al servidor localhost para lanzar la app nativa de Zoom
- En lugar de una solicitud AJAX normal, cargaba una imagen desde el servidor local de Zoom y expresaba los errores y códigos de estado del servidor mediante distintos tamaños de imagen
- La idea de que el navegador ignora la política de CORS de los servidores localhost es incorrecta, y Chrome respeta los headers de CORS de los servidores web en localhost
- Incluso cuando un frontend de Create React App y una API backend se ejecutan en distintos puertos de localhost, se generan solicitudes cross-origin, y esto está soportado por todos los navegadores
- Parece que, al bloquearse las solicitudes AJAX, Zoom optó por evadir CORS con este truco de imágenes
- Como resultado, no solo el sitio web de Zoom, sino también otros sitios de internet podían activar acciones del cliente nativo y acceder a la respuesta
Alternativas seguras y el problema de UX que sigue pendiente
- Una implementación segura sería que el servidor web en
localhost:19421implementara una API REST y configurara el headerAccess-Control-Allow-Origincon el valorhttps://zoom.us- Así, solo el JavaScript que se ejecuta en el dominio zoom.us podría comunicarse con el servidor web local
- zoom.us también podría usar un header de Content Security Policy para bloquear el renderizado en iframes y evitar que una reunión de Zoom se abra automáticamente en segundo plano
- Aun así, seguiría existiendo el problema de que cualquier página podría redirigir el navegador a un enlace de reunión de zoom.us
- Pero eso se parece más a la experiencia de usuario que Zoom eligió que a una vulnerabilidad de software
- Zoom rompe la expectativa del usuario de que, al hacer clic en un enlace, su cámara y micrófono no se abrirán de repente para personas desconocidas
- Si quieren evitar el popup nativo del navegador por razones de UX, también podrían mostrar un popup dentro de la app; Google Meet usa bien ese enfoque
- Ejecutar un servidor web en localhost ya es de por sí una decisión riesgosa, y en particular no se deberían ofrecer a todos los sitios web de internet funciones privilegiadas como la instalación de software
- CORS existe para manejar este tipo de situaciones de forma segura, así que no debería evadirse
La confusión sobre CORS no es un error exclusivo de Zoom
- No está claro si Zoom eligió este enfoque porque realmente no entendía CORS
- lerunicorn en Reddit sugiere que Firefox podría bloquear XHR desde un origen seguro hacia un origen inseguro
- Pero Firefox sí lo soporta cuando el origin es localhost
- Las apps nativas pueden generar su propio certificado autofirmado, y también pueden usar extensiones del navegador
- En ningún caso eso justifica omitir el filtrado por origen
- La confusión sobre CORS no es un problema exclusivo de Zoom
- En Stack Overflow hay muchas preguntas relacionadas con
Access-Control-Allow-Origin - Entre los ejemplos de Express, hay páginas que recomiendan valores predeterminados inseguros que, si se copian tal cual, pueden volver vulnerable una aplicación
- Otros vendors también han sufrido la misma vulnerabilidad que Zoom
- En Stack Overflow hay muchas preguntas relacionadas con
- Los desarrolladores quieren que su código funcione, pero si evitan por completo la política del mismo origen, como en el caso de Zoom, los privilegios locales quedan expuestos a sitios web externos
- La confusión sobre CORS aparece tanto en desarrolladores experimentados como en quienes recién empiezan; no está claro si el API de CORS es demasiado complejo o si falta formación sobre CORS y CSP, pero el enfoque actual no está funcionando bien
1 comentarios
Comentarios en Hacker News
Parece que el TFA tampoco entendió bien CORS o lo explicó muy mal
Access-Control-Allow-Origin: https://zoom.usno garantiza que solo el JavaScript del dominio zoom.us pueda comunicarse con el servidor en localhost. El JavaScript de otros sitios web también puede enviar solicitudes alocalhost:19421exactamente igual. CORS no restringe algo; es un mecanismo para relajar una restricción predeterminada. Ese header solo permite que el JavaScript que corre en zoom.us pueda leer la respuesta delocalhost:19421; la solicitud en sí ocurre de todos modos, así que el backend debe asegurarse de que no haya efectos secundariosLas solicitudes GET sí se envían, pero en principio deben ser idempotentes, así que si el servidor está bien implementado no pueden causar efectos secundarios, y en GET lo importante es si se puede leer la respuesta. En cambio, para las solicitudes no idempotentes que sí pueden tener efectos secundarios, en un contexto cross-origin primero se envía una solicitud preflight OPTIONS en lugar de la solicitud real, y si la respuesta a OPTIONS no tiene los headers correctos, la solicitud real no se envía
Los malentendidos sobre CORS están tan extendidos y la documentación a menudo se contradice tanto, que es difícil esperar que la otra parte lo haya implementado correctamente. Cuando un protocolo genera este nivel de confusión de forma tan generalizada, aunque un lado funcione bien no sabes si el otro también. Si la gente fue corrigiendo su código hasta que funcionara con otras implementaciones, también se vuelve borroso si el error estaba de su lado o del lado ajeno
Por ejemplo, un POST con
Content-Typeigual atext/jsonno puede enviarse a un host de terceros sin un preflight OPTIONS, pero un POST conmultipart/form-datasí está permitido y CORS no lo bloquea. Y si el endpoint no valida estrictamenteContent-Typey asume que es JSON, entonces cualquier sitio web podría enviar un POST sin interacción del usuarioUn desarrollador web competente no debería hacer que GET/HEAD/OPTIONS cambien el estado, y cosas como unirse a una reunión sí son cambios de estado. PUT/DELETE también deben ser idempotentes. Las APIs POST que no usan JSON ni formularios deben validar el header
Content-Type, y los POST conPUT/PATCH/DELETEy conContent-Typeque no sea de formulario disparan preflight, así que CORS se valida antes de que la solicitud real llegue al servidorNo basta con crear el certificado; también tiene que instalarse como certificado raíz de CA en todos los almacenes de confianza de los navegadores de la máquina. Si la clave privada de la CA raíz no está bien protegida, cualquier sitio web podría hacer un ataque de intermediario, así que como mínimo hacen falta restricciones de nombre(https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10). Pero Chrome no soportó esto en una CA raíz hasta la v112 en 2023(https://alexsci.com/blog/name-non-constraint/), así que había que agregar una CA intermedia y poner ahí la restricción. Por supuesto, lo correcto es desechar la clave de la CA raíz
Hace tiempo agregué restricciones básicas en un proyecto que usaba una CA raíz local, pero las puse mal en la CA raíz y ni siquiera lo probé en todos los navegadores
Ojalá más gente leyera la documentación de CORS de MDN. Me ayudó mucho cuando trataba de entender CORS, y viendo los comentarios aquí no sabía que a la gente le costaba tanto
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
Lo difícil de entender no es solo CORS; muchos desarrolladores tampoco entienden bien el modelo de amenazas
Aunque escuchan la explicación, muchas veces no logran dimensionar por qué es un problema serio. En especial, a menudo los desarrolladores backend son quienes configuran CORS, pero como CORS no es un mecanismo de protección de permisos de acceso, desde backend no parece tan importante. El atacante da la impresión de que no puede llevarse nada, y desde frontend puede verse como un obstáculo molesto. Este artículo muestra buenos ejemplos concretos
Como responsable de operaciones, lo corregí bien desde el balanceador de carga, y al menos ahora la aplicación funciona. CORS es difícil de entender, pero más triste aún es que también hay muchos desarrolladores que no entienden ni el modelo de amenazas que CORS intenta bloquear ni el desarrollo web en general, especialmente el protocolo HTTP
multipart/form-dataestá bien, pero el JavaScript de la aplicación noCORS es opcional, y otras librerías o herramientas simplemente pueden ignorarlo. En la práctica, CORS solo tiene sentido para frenar XSS y CSRF contra un usuario humano que realmente inició sesión; en otros escenarios de ataque no sirve de nada, porque igual usarán scripts o programas que falsifican headers HTTP. Por eso la gente termina habilitando todas las opciones de CORS, y ese es el peor caso posible porque permite XSS y CSRF
Esta sección de comentarios realmente se ve de muy bajo nivel informativo y, de hecho, demuestra exactamente el punto del autor
Si hacías desarrollo web antes de que existiera CORS, entendías que las solicitudes entre dominios estaban prohibidas por defecto y que CORS apareció para sortear esa restricción de seguridad. Por eso es fácil asumir que, si quieres hacer algo, simplemente habilitas CORS.
En cambio, quien aprendió desarrollo web después de CORS solo ve el flujo de intentar una solicitud de origen cruzado, que el navegador decida que no está permitida, intente un preflight de CORS y, si falla, aparezca un error de CORS en la consola. Si no conoces cómo funciona internamente y no has leído la documentación, es fácil pensar que CORS es la causa de que la solicitud esté bloqueada e intentar “desactivar CORS”. Pero CORS no es la causa del problema, sino la solución.
Como otras personas con el mismo malentendido lo repiten con seguridad en tutoriales y discusiones en línea, todo se vuelve aún más confuso
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
Al leer los comentarios confirmé que no era solo yo. Nadie entiende CORS porque es demasiado complejo y tiene muchos choques.
Los estándares y los headers también siguen cambiando, así que la mayoría de los desarrolladores solo mueven cosas hasta que funciona, despliegan el producto y lo dejan así. Incluso si funciona, puede que queden errores y advertencias en la consola del desarrollador, pero mientras por fuera parezca andar bien, ya no lo tocan
Para entender CORS, primero hay que entender la política del mismo origen
En particular, si te cuesta responder “¿por qué esto es necesario?”, conviene empezar aquí: https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Same-origin_policy
Antes llegué a usar la política del mismo origen como pregunta de entrevista, pero muchos candidatos no estaban familiarizados con ella, así que esa pregunta no aportaba mucha información
Si has desarrollado aplicaciones web, en algún momento deberías haberte topado con la política del mismo origen. Si no la conoces, normalmente dan ganas de preguntar más sobre cómo se comunicaban con el backend, por ejemplo. En algunos roles, también es una señal útil saber si se encontraron con problemas de CORS pero solo aplicaron el atajo más rápido y se olvidaron del tema, o si realmente intentaron entenderlo.
Para un rol backend es menos adecuada, porque no todos los desarrolladores backend han trabajado de cerca con equipos frontend que se topan con problemas de CORS con frecuencia
Lo que recuerdo de CORS es que depurarlo toma muchísimo más tiempo de lo esperado, que los mensajes de error del navegador son intencionalmente escasos y que al principio es difícil distinguir un error de CORS de otros modos de fallo
Claro, si el servidor no entiende la solicitud CORS y devuelve una respuesta rara, eso al final puede traducirse en un fallo de CORS
Ya que la sección de comentarios está bastante entretenida, agrego esto: la política del mismo origen protege para que el navegador no filtre información hacia sitios web sin permiso, y CORS permite debilitar esa protección.
Por ejemplo, la política del mismo origen impide que
example.comobtenga la lista de suscripciones deyoutube.com. Pero con CORS, se puede permitir queexample.comacceda ayoutube.com/public/*.Otro uso es evitar que una API backend funcione debajo de otro frontend y termine facilitando robo de datos. Por ejemplo, ayuda a prevenir una situación donde el usuario sí inició sesión en el servicio real, pero está en
g00gle.com, y todas las solicitudes podrían ser interceptadas por un ataque de intermediarioYo también soy una de esas personas. CORS es un tema que tengo que volver a estudiar periódicamente, y siempre se me olvida, así que no se me queda en la cabeza.
Supongo que es porque soy desarrollador backend y casi nunca me topo con problemas de CORS. Suelo olvidar bien las cosas que no uso todos los días
En un mundo normal, el mensaje de error incluiría pistas como “header de respuesta” o “meta tag”, pero parece que los principales fabricantes de navegadores contrataron a gente especializada en escribir mensajes crípticos. El “requested resource” de Chrome es de lo mejorcito, pero sigue pareciendo un acertijo.
Un mensaje mejor sería algo como que el recurso de
https://bank.comno permite solicitudes de origen cruzado porque no tiene headers de CORS, o que el origen actual no está en la lista de permitidos por CORS. También debería mostrar la solicitud preflight en la pestaña de red y un enlace a MDN. Con CSP también sería mejor algo como que no se puede obtener el recurso por el header de CSP de esta página, y enlazar al header de solicitud de la página o al meta tag desde el inspectorAl final, casi siempre depende de asumir que el servidor solo será accedido mediante solicitudes de navegador no manipuladas. La vulnerabilidad de Zoom surgió porque del lado del cliente era demasiado fácil saltarse CORS y CSP, y aunque es cierto que Zoom fue malo, flojo y tonto, siento que la comunidad que sigue manteniendo este modelo también tiene parte de la culpa
Entiendo cómo la política del mismo origen evita que el navegador ejecute scripts maliciosos y filtre información. También entiendo que el servidor declare que confía en orígenes adicionales y relaje SOP con el header
Access-Control-Allow-Origin.Aun así, todavía no entiendo para qué sirve el header
Access-Control-Allow-Headers. No parece mejorar la seguridad del navegador, y menos aún la del servidor. Me pregunto si el diseñador del protocolo lo puso “por completitud”. Relacionado: https://stackoverflow.com/questions/17992042