Lorsque j'étais chez PKC, notre équipe a effectué plus de vingt audits de code, dont beaucoup pour des startups qui étaient sur le point d'atteindre leur série A ou B (c'est généralement à ce moment-là qu'elles avaient du cash et qu'elles ont réalisé qu'il serait bon d'examiner de plus près leur sécurité, après s'être concentrées sur le product market fit).
C'était un travail fascinant - nous avons plongé dans un grand nombre de stacks et d'architectures, dans une grande variété de domaines. Nous avons découvert toutes sortes de problèmes de sécurité, allant de catastrophiques à tout simplement intéressants. Nous avons également eu l'occasion de discuter avec des responsables de l'ingénierie et, plus généralement, avec des CTO, des défis en matière d'ingénierie et de produits auxquels ils étaient confrontés alors qu'ils commençaient à se développer.
C'est également fascinant de voir lesquelles de ces startups ont bien réussi et lesquelles ont disparu, maintenant que certains de ces audits remontent à 7 ou 8 ans.
Je souhaite vous faire part de certaines des choses les plus surprenantes que j'ai intériorisées à partir de ces observations, classées grossièrement du plus général au plus spécifique à la sécurité.
- Il n'est pas nécessaire d'avoir des centaines d'ingénieurs pour créer un excellent produit. J'ai écrit un article plus long à ce sujet, mais en gros, bien que le stade général des startups que nous avons auditées soit assez similaire, la taille des équipes d'ingénieurs variait beaucoup. Il est surprenant de constater que les produits les plus impressionnants, dotés des fonctionnalités les plus étendues, étaient parfois construits par des équipes plus petites. Et ce sont ces mêmes équipes "petites mais puissantes" qui, des années plus tard, écrasent leurs marchés.
- La simplicité l'emporte sur le génie. En tant qu'élitiste avoué, cela me fait mal de le dire, mais c'est vrai : les start-ups que nous avons auditées et qui obtiennent aujourd'hui les meilleurs résultats avaient généralement une approche presque effrontément "simple" (keep-it-simple) de l'ingénierie. L'ingéniosité pour l'ingéniosité était abhorrée. D'un autre côté, les entreprises pour lesquelles nous nous disions "woah, ces gens sont très intelligents" ont pour la plupart disparu. En général, le principal tir dans le pied (dont je parle plus en détail dans un précédent billet sur les tirs dans le pied) qui a mis beaucoup d'entreprises en difficulté a été le passage prématuré aux microservices, à des architectures reposant sur l'informatique distribuée et à des structures à forte teneur en communications par messages.
- Nos constatations ayant le plus grand impact sont toujours faites dans les premières et dernières heures de l'audit. Si vous y réfléchissez, c'est logique : dans les premières heures de l'audit, vous trouvez les fruits les plus faciles à cueillir. Les choses qui ressortent comme un cheveu sur la soupe en parcourant le code et en testant quelques fonctionnalités de base. Au cours des dernières heures, vous vous êtes entièrement familiarisé avec la nouvelle base de code, et les choses commencent à se mettre en place.
- Écrire des logiciels sûrs est devenu remarquablement plus facile au cours des dix dernières années. Je n'ai pas de preuves statistiques solides pour étayer cette affirmation, mais il semble que le code écrit avant 2012 environ avait tendance à présenter beaucoup plus de vulnérabilités par SLOC que le code écrit après 2012 (nous avons commencé les audits en 2014). Peut-être était-ce dû aux frameworks Web 2.0 ou à une sensibilisation accrue des développeurs à la sécurité. Quoi qu'il en soit, je pense que cela signifie que la sécurité s'est vraiment améliorée sur un plan fondamental en termes d'outils et de paramètres par défaut dont disposent les ingénieurs logiciels.
- Toutes les vulnérabilités de sécurité vraiment mauvaises étaient évidentes. Dans environ un cinquième des audits de code que nous réalisions, nous trouvions La Grande Faille - une vulnérabilité si grave que nous appelions nos clients pour leur dire de la corriger immédiatement. Je ne me souviens pas d'un seul cas où cette vulnérabilité était très astucieuse. En fait, c'est en partie ce qui rendait les pires vulnérabilités dangereuses - nous étions inquiets principalement parcequ'elles étaient faciles à trouver et à exploiter. La "découvrabilité" est un élément de l'analyse d'impact depuis un certain temps, ce n'est donc pas nouveau. Mais je pense que la découvrabilité devrait être beaucoup plus fortement prise en compte. La découvrabilité est primordiale, lorsqu'il s'agit du risque réel. Les pirates sont paresseux et recherchent les proies les plus faciles. Ils ne s'intéresseront pas à la découverte d'une vulnérabilité de type "heap-spray", même très grave, s'ils peuvent réinitialiser le mot de passe d'un utilisateur parce que le jeton de réinitialisation se trouvait dans la réponse (comme Uber l'a découvert vers 2016). Le contre-argument à ça est que pondérer fortement la découvrabilité perpétue la ”Sécurité par l'Obscurité,” puisqu'elle repose largement sur l'estimation de ce qu'un attaquant peut ou devrait savoir. Mais là encore, l'expérience personnelle suggère fortement qu'en pratique, la découvrabilité est un excellent prédicteur des risques d'exploitation réels.
- Les fonctions de sécurité par défaut dans les frameworks et les infrastructures ont massivement amélioré la sécurité. J'ai également écrit un article plus long à ce sujet, mais essentiellement, des choses comme le fait que React échappe par défaut tout le HTML pour éviter les scripts cross-site et que les stacks sans serveur retirent la configuration du système d'exploitation et du serveur web des mains des développeurs, ont considérablement amélioré la sécurité des entreprises qui les utilisent. Comparez ça à nos audits PHP, qui étaient truffés de XSS… Ces nouvelles stacks et frameworks ne sont pas impénétrables, mais leur surface attaquable est plus petite, précisément aux endroits qui font une différence considérable dans la pratique.
- Les monorépos sont plus faciles à auditer. Du point de vue de l'ergonomie des chercheurs en sécurité, il était plus facile d'auditer un monorépo qu'une série de services répartis dans différentes bases de code. Il n'était pas nécessaire d'écrire des scripts enveloppes (wrappers) autour des différents outils dont nous disposions. Il était plus facile de déterminer si un morceau de code donné était utilisé ailleurs. Et surtout, il n'y avait pas besoin de s'inquiéter de la version d'une bibliothèque commune qui serait différente sur un autre dépôt.
- Vous pourriez facilement passer un audit entier à parcourir la piste des bibliothèques de dépendances vulnérables.Il est incroyablement difficile de dire si une vulnérabilité donnée dans une dépendance est exploitable. En tant qu'industrie, nous sommes définitivement sous-investis dans la sécurisation des bibliothèques de base, c'est pourquoi des choses comme Log4j ont eu un tel impact. Node et npm étaient absolument terrifiants à cet égard - les chaînes de dépendances n'étaient tout simplement pas auditables. Le lancement de dependabot par GitHub a été une véritable aubaine, car nous pouvions, pour la plupart, dire à nos clients de mettre à niveau les dépendances par ordre de priorité.
- Ne jamais désérialiser les données non fiables. Cela s'est produit le plus souvent en PHP, car pour une quelconque raison, les développeurs PHP aiment sérialiser/désérialiser les objets au lieu d'utiliser JSON, mais je dirais que presque tous les cas que nous avons vus où un serveur désérialisait un objet client et l'analysait ont conduit à un exploit horrible. Pour ceux d'entre vous qui ne sont pas familiers avec le sujet, Portswigger a fait une bonne analyse de ce qui peut se passer (d'ailleurs, il se concentre sur PHP. Coïncidence ?). En bref, le point commun de toutes les vulnérabilités de désérialisation est que donner à un utilisateur la possibilité de manipuler un objet qui est ensuite utilisé par le serveur est une capacité extrêmement puissante avec une large surface d’attaque. C'est conceptuellement similaire à la pollution de prototypes et aux modèles HTML générés par l'utilisateur. La solution ? Il est de loin préférable de permettre à un utilisateur d'envoyer un objet JSON (il y a si peu de types de données possibles), et de construire manuellement l'objet en fonction des champs de cet objet. C'est un peu plus de travail, mais cela en vaut la peine !
- Les failles au niveau de la logique métier sont rares, mais quand nous en trouvons une, elle est généralement très mauvaise. Pensez-y : des bugs dans la logique métier sont garantis d’affecter l'activité. Un corollaire intéressant est que même si votre protocole est construit pour offrir des propriétés de sécurité prouvées, l'erreur humaine sous la forme d'une mauvaise logique métier est étonnamment courante (il suffit de voir la série d'exploits absolument dévastateurs qui tirent parti de smart contracts mal écrits).
-
Le fuzzing personnalisé a été étonnamment
efficace. Après quelques années d'audit de code, j'ai commencé à
exiger que tous nos audits de code incluent la création de
fuzzers personnalisés pour tester les API de produits,
l'authentification, etc. C'est une pratique assez courante,
et j'ai volé cette idée à Thomas Ptacek, qui y fait allusion
dans son
Hiring Post. Avant que nous ne le fassions, je pensais en fait que
c'était une perte de temps - j'ai toujours pensé que c'était
un exemple d'ingénierie mal appliquée, et que les heures
d'audit étaient mieux utilisées à lire du code et à essayer
diverses hypothèses. Mais il s'est avéré que le fuzzing
était étonnamment efficace et efficient en termes d'heures
passées, en particulier sur les bases de code les plus
importantes.
- Les rachats ont passablement compliqué la sécurité. Il y avait plus de modèles de code à vérifier, plus de comptes AWS à examiner, plus de variété dans les outils SDLC. Et bien sûr, le rachat se traduit généralement par l'utilisation d'un langage et/ou d'un framework entièrement nouveau, avec ses propres schémas.
- Il y avait toujours au moins un enthousiaste de la sécurité caché parmi les ingénieurs logiciels. C'était toujours surprenant de savoir qui c'était, et ils ne savaient presque jamais que c'était eux ! Comme les compétences en matière de sécurité se répartissent de plus en plus entre les logiciels, les possibilités d'arbitrage sont énormes si ces personnes peuvent être identifiées de manière fiable.
- La rapidité de correction des vulnérabilités est généralement corrélée à l'excellence opérationnelle de l'ingénierie dans son ensemble. Dans les meilleurs cas, les clients nous demandaient de les tenir informés en permanence de tout ce que nous trouvions pour qu'ils puissent corriger le problème immédiatement.
- Presque personne n'a réussi à maîtriser les jetons JWT et les webhooks du premier coup. Avec les webhooks, les gens oubliaient presque toujours d'authentifier les requêtes entrantes (ou le service qu'ils utilisaient ne permettait pas l'authentification... ce qui était plutôt foireux !). Ce type de problème a conduit Josh, l'un de nos chercheurs, à poser une série de questions qui ont abouti à une présentation à la DefCON/Blackhat. JWT est notoirement difficile à mettre en place, même si vous utilisez une bibliothèque, et il y avait beaucoup d'implémentations qui n'arrivaient pas à faire expirer correctement les tokens lors de la déconnexion, qui ne vérifiaient pas correctement l'authenticité des JWT, ou qui leur faisaient simplement confiance par défaut.
- Il y a encore beaucoup de MD5 utilisés, mais ce sont surtout des faux positifs. Il s'avère que le MD5 est utilisé pour beaucoup d'autres choses qu'un hachage de mot de passe (in)suffisamment résistant aux collisions. Par exemple, en raison de sa rapidité, il est souvent utilisé dans les tests automatisés pour générer rapidement un grand nombre de GUID pseudo-aléatoires. Dans ces cas, les propriétés non sécurisées du MD5 n'ont pas d'importance, malgré ce que votre outil d'analyse statique peut vous dire.
Je suis curieux de savoir si vous avez vu l'un d'entre eux, ainsi que d'autres ! Ou, si vous n'êtes pas d'accord, écrivez-moi un mot !