2 puntos por GN⁺ 4 시간 전 | 1 comentarios | Compartir por WhatsApp
  • 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:19421 y, 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-Origin para 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:19421 en 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:19421 implementara una API REST y configurara el header Access-Control-Allow-Origin con el valor https://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
  • 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

 
GN⁺ 4 시간 전
Comentarios en Hacker News
  • Parece que el TFA tampoco entendió bien CORS o lo explicó muy mal
    Access-Control-Allow-Origin: https://zoom.us no 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 a localhost:19421 exactamente 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 de localhost:19421; la solicitud en sí ocurre de todos modos, así que el backend debe asegurarse de que no haya efectos secundarios

    • No entiendo por qué este es el comentario con más votos. El OP tiene razón, y la explicación de arriba está equivocada
      Las 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
    • Tampoco diría que CORS cumple exactamente ese papel
      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
    • Según lo entiendo, el propósito principal de preflight OPTIONS es bloquear solicitudes HTTP que originalmente no estarían permitidas, y en las que sí lo están CORS no hace nada
      Por ejemplo, un POST con Content-Type igual a text/json no puede enviarse a un host de terceros sin un preflight OPTIONS, pero un POST con multipart/form-data sí está permitido y CORS no lo bloquea. Y si el endpoint no valida estrictamente Content-Type y asume que es JSON, entonces cualquier sitio web podría enviar un POST sin interacción del usuario
    • Asumir que “solo hablamos de métodos seguros” es una suposición bastante grande
      Un 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 con PUT/PATCH/DELETE y con Content-Type que no sea de formulario disparan preflight, así que CORS se valida antes de que la solicitud real llegue al servidor
    • También hay un problema con la parte del artículo que dice que “una app nativa puede generar su propio certificado autofirmado
      No 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

    • Ese documento por sí solo responde la mayoría de las dudas, no solo sobre casos simples de origen sino también sobre cómo funciona preflight
  • 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

    • Incluso en proyectos donde el mismo desarrollador escribió tanto el frontend como el backend, he visto configuraciones de CORS mal hechas
      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
    • El modelo de amenazas de CORS no es tan difícil. Se trata de una situación en la que el atacante lleva al usuario a su sitio y hace que realice alguna acción en tu sitio
    • CORS confunde porque está montado sobre un modelo de permisos predeterminado bastante raro. Algo como multipart/form-data está bien, pero el JavaScript de la aplicación no
    • Visto desde la perspectiva del atacante y del defensor, el modelo de amenazas no resulta del todo natural
      CORS 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
    • CORS es excelente para evitar que la gente robe fácilmente ancho de banda y recursos de hosting. Para hacerlo tendrían que montar su propio proxy, y entonces es más fácil bloquearlos
  • Esta sección de comentarios realmente se ve de muy bajo nivel informativo y, de hecho, demuestra exactamente el punto del autor

    • También puede ser una diferencia generacional
      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
    • CORS no es intuitivo, pero si lees la documentación se puede entender
      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

    • Me parece una muy buena pregunta al contratar desarrolladores frontend
      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

    • Un error de CORS no es un “mensaje de error enviado al navegador”, sino un error que el navegador genera al decidir que no puede permitir la solicitud.
      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.com obtenga la lista de suscripciones de youtube.com. Pero con CORS, se puede permitir que example.com acceda a youtube.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 intermediario

    • En realidad es al revés. Lo que evita ese problema de seguridad es SOP, y CORS es la función que relaja SOP para permitir comportamientos más complejos entre aplicaciones
  • Yo 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

    • La experiencia de desarrollador con CORS y CSP es terrible. Porque los navegadores no dicen bien de dónde viene el problema.
      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.com no 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 inspector
    • El mayor problema de CORS es que la mayoría de los errores parecen problemas de frontend, en especial problemas del navegador, pero la corrección real hay que hacerla en el backend
    • A mí me pasa algo parecido. Las veces que tuve que lidiar con CORS fueron situaciones del tipo “hay que traer algo de este servidor, pero no se puede cambiar el CORS ni el CSP del servidor”, lo que en términos de seguridad significa “hay un sistema de seguridad y hay que rodearlo”.
      Al 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