Les développeurs ne comprennent pas CORS (2019)
(fosterelli.co)- La vulnérabilité du serveur web local de Zoom montre à quel point une frontière de sécurité peut facilement s’effondrer lorsque de nombreux développeurs web comprennent mal le fonctionnement de CORS
- En communiquant avec le serveur local
localhost:19421, Zoom transmettait les codes d’état via la taille d’image au lieu d’utiliser AJAX, ce qui s’apparente à une implémentation de contournement pour éviter CORS - Chrome applique aussi les en-têtes CORS aux serveurs web localhost, et la communication frontend/backend sur différents ports localhost est également prise en charge par les navigateurs
- Une conception plus sûre consisterait à ce que le serveur local fournisse une API REST et définisse
Access-Control-Allow-Originpour limiter l’accès au seul JavaScript de zoom.us - Contourner la politique de même origine peut faire fonctionner le code, mais cela peut exposer les fonctions privilégiées du serveur local à tous les sites web sur Internet
Le contournement de CORS créé par le serveur web local de Zoom
- En travaillant dans le conseil full stack auprès de développeurs de tailles d’entreprise et de secteurs variés, l’auteur a constaté de manière répétée que les développeurs web ne comprenaient pas bien CORS
- Dans la récente vulnérabilité de Zoom, le chercheur en sécurité Jonathan Leitschuh a découvert que Zoom lançait un serveur web
http://localhost:19421sur la machine de l’utilisateur- Lorsqu’un utilisateur ouvre un lien Zoom, le site web de Zoom envoie une requête au serveur localhost pour lancer l’application Zoom native
- Au lieu d’une requête AJAX classique, Zoom chargeait une image depuis le serveur web local et utilisait des tailles d’image différentes pour représenter les erreurs et codes d’état du serveur
- L’idée selon laquelle le navigateur ignorerait la politique CORS d’un serveur localhost est fausse, et Chrome respecte les en-têtes CORS des serveurs web localhost
- Même lorsque le frontend Create React App et l’API backend tournent sur des ports localhost différents, il s’agit bien de requêtes cross-origin, et cela est pris en charge par tous les navigateurs
- Il semble que Zoom ait contourné CORS via cette astuce d’image après avoir vu ses requêtes AJAX bloquées
- En conséquence, non seulement le site de Zoom, mais aussi d’autres sites web sur Internet pouvaient déclencher le comportement du client natif et accéder aux réponses
Une alternative plus sûre et les problèmes d’UX qui subsistent
- Une implémentation sûre consisterait à faire en sorte que le serveur web sur
localhost:19421expose une API REST et définisse l’en-têteAccess-Control-Allow-Originsurhttps://zoom.us- Ainsi, seul le JavaScript exécuté depuis le domaine zoom.us pourrait communiquer avec le serveur web localhost
- zoom.us pourrait aussi définir un en-tête Content Security Policy bloquant le rendu en iframe afin d’éviter qu’une réunion Zoom ne s’ouvre automatiquement en arrière-plan
- Le problème selon lequel n’importe quelle page peut toujours rediriger le navigateur vers un lien de réunion zoom.us demeure
- Cela relève toutefois davantage de l’expérience utilisateur choisie par Zoom que d’une vulnérabilité logicielle
- Zoom rompt l’attente des utilisateurs selon laquelle cliquer sur un lien ne devrait pas ouvrir soudainement leur caméra et leur micro pour des inconnus
- Si l’objectif est d’éviter la popup native du navigateur pour des raisons d’UX, l’application pourrait afficher sa propre popup, comme le fait efficacement Google Meet
- Faire tourner un serveur web sur localhost est en soi une approche risquée, et il ne faut surtout pas offrir à tous les sites web d’Internet des fonctions privilégiées comme l’installation de logiciels
- CORS existe précisément pour gérer ce type de situation en sécurité, et il ne faut donc pas le contourner
La confusion autour de CORS ne concerne pas seulement Zoom
- Il n’est pas certain que Zoom ait choisi cette approche parce que l’entreprise ne comprenait réellement pas CORS
- lerunicorn sur Reddit estime que Firefox peut bloquer les XHR depuis une origine sécurisée vers une origine non sécurisée
- Mais Firefox le prend en charge lorsque l’origine est localhost
- Une application native peut générer son propre certificat autosigné, et il est aussi possible d’utiliser une extension de navigateur
- Dans tous les cas, cela ne justifie en rien l’absence de filtrage par origine
- La confusion autour de CORS n’est pas propre à Zoom
- Stack Overflow contient de très nombreuses questions sur
Access-Control-Allow-Origin - Parmi les exemples Express, certaines pages recommandent des valeurs par défaut dangereuses qui peuvent rendre une application vulnérable si elles sont copiées telles quelles
- D’autres éditeurs ont déjà connu la même vulnérabilité que Zoom
- Stack Overflow contient de très nombreuses questions sur
- Les développeurs veulent faire marcher leur code, mais contourner entièrement la politique de même origine expose, comme dans le cas de Zoom, des privilèges locaux à des sites web externes
- La confusion autour de CORS touche aussi bien les développeurs expérimentés que les débutants, et il n’est pas clair si l’API CORS est excessivement complexe ou si la formation sur CORS et CSP est insuffisante, mais l’approche actuelle ne fonctionne manifestement pas bien
1 commentaires
Commentaires Hacker News
Il semble que le TFA non plus n’ait pas vraiment compris CORS, ou l’explique très mal
Access-Control-Allow-Origin: https://zoom.usne garantit pas que seul le JavaScript du domaine zoom.us puisse communiquer avec le serveur localhost. Le JavaScript d’autres sites web peut tout autant envoyer des requêtes verslocalhost:19421. CORS ne sert pas à restreindre quelque chose, mais à assouplir une restriction par défaut. Cet en-tête permet seulement au JavaScript exécuté sur zoom.us de lire la réponse delocalhost:19421, mais la requête elle-même a lieu dans tous les cas, donc le backend doit être conçu pour éviter tout effet de bordLes requêtes GET sont bien envoyées, mais elles sont censées être idempotentes à l’origine, donc si le serveur est correctement implémenté, elles ne peuvent pas produire d’effets de bord, et pour GET, l’enjeu principal est de savoir si la réponse peut être lue. À l’inverse, pour les requêtes non idempotentes pouvant avoir des effets de bord, une requête preflight OPTIONS est d’abord envoyée à la place de la vraie requête en contexte cross-origin, et si la réponse OPTIONS ne contient pas les bons en-têtes, la requête réelle n’est pas envoyée
Les malentendus autour de CORS sont tellement répandus, et la documentation est souvent contradictoire, qu’il est difficile de s’attendre à ce qu’un interlocuteur inconnu l’ait correctement implémenté. Quand un protocole provoque une confusion aussi généralisée, même si une partie fonctionne correctement, on ne peut pas savoir si l’autre fera de même. Si les gens ont corrigé leur code jusqu’à ce qu’il marche avec d’autres implémentations, il devient flou de savoir si l’erreur venait de chez eux ou de l’autre côté
Par exemple, un POST avec
Content-Typeàtext/jsonne peut pas être envoyé vers un hôte tiers sans preflight OPTIONS, alors qu’un POST enmultipart/form-dataest autorisé et n’est pas bloqué par CORS. Et si l’endpoint ne vérifie pas strictement leContent-Typeet suppose qu’il s’agit de JSON, alors n’importe quel site web peut envoyer un POST sans action de l’utilisateurUn bon développeur web ne devrait pas faire en sorte que GET/HEAD/OPTIONS modifient l’état, et rejoindre une réunion est bien un changement d’état. PUT/DELETE doivent aussi être idempotentes. Une API POST qui n’utilise ni JSON ni formulaire doit vérifier l’en-tête
Content-Type, et lesPUT/PATCH/DELETEainsi que les POST avec unContent-Typeautre qu’un format de formulaire déclenchent un preflight, ce qui permet à CORS d’être vérifié avant que la requête réelle n’atteigne le serveurLe simple fait de créer un certificat ne suffit pas : il doit être installé comme certificat de CA racine dans tous les magasins de confiance des navigateurs de la machine. Si la clé privée de la CA racine n’est pas correctement protégée, n’importe quel site web peut effectuer une attaque de l’homme du milieu, donc il faut au minimum une contrainte de nom (https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10). Or Chrome ne faisait pas fonctionner cela avec une CA racine avant la v112 en 2023 (https://alexsci.com/blog/name-non-constraint/), il fallait donc ajouter une CA intermédiaire et y appliquer la restriction. Bien sûr, la bonne pratique reste de jeter la clé de la CA racine
Sur un ancien projet utilisant une CA racine locale, j’avais ajouté des contraintes de base, mais je les avais mises par erreur sur la CA racine et je n’avais pas testé sur tous les navigateurs
J’aimerais que davantage de gens lisent la documentation MDN sur CORS. Elle m’a beaucoup aidé à comprendre CORS, et en lisant les commentaires ici, je ne pensais pas que tant de gens avaient autant de mal avec le sujet
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
Ce n’est pas seulement CORS qui est difficile à comprendre : beaucoup de développeurs comprennent mal le modèle de menace
Même avec une explication, il est souvent difficile de percevoir pourquoi c’est un gros problème. En particulier, ce sont souvent les développeurs backend qui configurent CORS, mais comme CORS n’est pas un mécanisme de protection des autorisations d’accès, cela ne paraît pas très important côté backend. Les attaquants ont l’impression de ne rien pouvoir voler, tandis que côté frontend, cela ressemble facilement à un obstacle pénible. Cet article montre bien des exemples concrets
En tant qu’exploitant, je l’ai corrigée correctement au niveau du load balancer, et au moins l’application fonctionne désormais. CORS est difficile à comprendre, mais il est encore plus regrettable de voir combien de développeurs ne comprennent pas le modèle de menace que CORS essaie de contrer, ni le développement web en général, en particulier le protocole HTTP
multipart/form-dataest accepté, mais pas le JavaScript de l’application, par exempleCORS est optionnel, et d’autres bibliothèques ou outils peuvent tout simplement l’ignorer. En pratique, CORS n’a de sens que pour empêcher les XSS et CSRF visant un utilisateur humain réellement connecté ; pour les autres scénarios d’attaque, cela ne sert à rien, car on utilise de toute façon des scripts ou programmes capables de falsifier les en-têtes HTTP. C’est pour cela que les gens finissent souvent par activer toutes les options CORS, ce qui est le pire cas possible puisqu’on autorise alors XSS et CSRF
Cette section de commentaires semble vraiment d’un niveau d’information assez faible, et elle démontre plutôt exactement le propos de l’auteur
Si on faisait du développement web avant l’apparition de CORS, on sait qu’à l’origine les requêtes inter-domaines étaient interdites, et que CORS est apparu pour contourner cette sécurité. Il est donc facile d’intégrer l’idée qu’il suffit d’activer CORS pour faire ce qu’on veut
À l’inverse, quelqu’un qui a appris le développement web après CORS essaie une requête cross-origin, voit que le navigateur décide qu’elle n’est pas autorisée, tente un preflight CORS, puis, si ça échoue, voit une erreur CORS dans la console. Si on ne connaît pas le fonctionnement interne et qu’on n’a pas lu la documentation, on peut en déduire que CORS est la cause qui bloque la requête et chercher à « désactiver CORS ». Mais CORS n’est pas la cause du problème, c’est la solution
Comme des gens ayant la même confusion répètent cela avec assurance dans les tutoriels et les discussions en ligne, ça devient encore plus déroutant
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
En lisant les commentaires, j’ai constaté que je n’étais pas le seul. Si personne ne comprend CORS, c’est parce que c’est beaucoup trop complexe et plein de conflits
Les standards et les en-têtes changent sans arrêt, donc les développeurs bricolent généralement jusqu’à ce que ça marche, déploient le produit, et s’arrêtent là. Même quand ça fonctionne, il peut rester des erreurs et des avertissements dans la console développeur, mais si tout semble bien marcher en apparence, on évite simplement d’y toucher
Pour comprendre CORS, il faut d’abord comprendre la policy de même origine
Surtout si la question « pourquoi est-ce nécessaire ? » vous paraît difficile, mieux vaut commencer ici : https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Same-origin_policy
J’ai déjà utilisé la policy de même origine comme question d’entretien, mais beaucoup de candidats ne la connaissaient pas, donc cette question apportait peu d’informations utiles
Si quelqu’un a développé des applications web, il a forcément dû rencontrer un jour la policy de même origine. S’il ne la connaît pas, ça amène généralement à poser plus de questions sur la façon dont il communiquait avec le backend, par exemple. Selon le poste, savoir s’il a rencontré un problème CORS mais s’est contenté d’appliquer le contournement le plus rapide avant de l’oublier, ou s’il a réellement essayé de le comprendre, peut être un signal utile
C’est moins adapté à un poste backend, car tous les développeurs backend n’ont pas travaillé de près avec une équipe frontend confrontée fréquemment à des problèmes CORS
Ce dont je me souviens à propos de CORS, c’est que le débogage prend bien plus de temps que prévu, que les messages d’erreur du navigateur sont volontairement pauvres, et qu’au début il est difficile de distinguer une erreur CORS d’autres modes d’échec
Bien sûr, si le serveur ne comprend pas les requêtes CORS et renvoie une réponse étrange, cela peut au final se traduire par un échec CORS
En voyant cette section de commentaires, je trouve ça assez amusant, alors j’ajoute ceci : la policy de même origine protège contre l’exfiltration d’informations vers des sites web auxquels le navigateur n’a pas donné accès, et CORS permet d’affaiblir cette protection
Par exemple, la policy de même origine empêche
example.comde récupérer la liste des abonnements deyoutube.com. Mais avec CORS, on peut autoriserexample.comà accéder àyoutube.com/public/*Un autre usage est d’empêcher qu’une API backend fonctionne sous un autre frontend et mène à un vol de données. Par exemple, cela évite une situation où l’utilisateur est bien connecté au vrai service, mais se trouve sur
g00gle.com, et où toutes les requêtes pourraient faire l’objet d’une attaque de l’homme du milieuJ’en fais aussi partie. CORS est un sujet qu’il faut réétudier périodiquement, et je l’oublie toujours, donc ça ne reste pas bien en tête
C’est sans doute parce que je suis développeur backend et que je rencontre rarement des problèmes CORS. J’ai tendance à oublier ce que je n’utilise pas tous les jours
Dans un monde normal, les messages d’erreur donneraient des indices comme « en-tête de réponse » ou « balise meta », mais on dirait que les grands éditeurs de navigateurs embauchent des gens spécialisés dans les messages énigmatiques. Le « requested resource » de Chrome est un peu mieux, mais reste cryptique
Un meilleur message dirait par exemple que la ressource
https://bank.comn’autorise pas les requêtes cross-origin parce qu’elle n’a pas d’en-tête CORS, ou que l’origine actuelle ne figure pas dans la liste autorisée par CORS. Il devrait aussi montrer la requête preflight dans l’onglet réseau, avec un lien vers MDN. Pour CSP aussi, il vaudrait mieux indiquer que cette page ne peut pas récupérer la ressource à cause de l’en-tête CSP de la page, et faire le lien vers les en-têtes de requête de la page ou la balise meta dans l’inspecteurAu final, cela repose généralement sur l’hypothèse que le serveur n’est accessible qu’à des requêtes navigateur non modifiées. La vulnérabilité de Zoom existait parce qu’il était trop facile de contourner CORS et CSP côté client, et même si Zoom a effectivement été mauvais, paresseux et stupide, j’ai l’impression que la communauté qui continue à maintenir ce modèle porte aussi une part de responsabilité
Je comprends comment la policy de même origine empêche le navigateur d’exécuter des scripts malveillants pour exfiltrer des informations. Je comprends aussi qu’avec l’en-tête
Access-Control-Allow-Origin, le serveur déclare faire confiance à des origines supplémentaires et assouplit ainsi la SOPEn revanche, je ne comprends toujours pas à quoi sert l’en-tête
Access-Control-Allow-Headers. Il ne semble pas améliorer la sécurité du navigateur, et encore moins celle du serveur. Je me demande si les concepteurs du protocole l’ont ajouté « pour faire complet ». À ce sujet : https://stackoverflow.com/questions/17992042