De nos jours, il n'y a pratiquement personne qui n'ait jamais cliqué sur le bouton "Récupérer le mot de passe" dans une profonde frustration. Même s'il semble que le mot de passe était sans aucun doute correct, la prochaine étape de récupération se déroule généralement sans problème, en visitant un lien provenant d'un e-mail et en saisissant le nouveau mot de passe (ne trompons personne ; ce n'est pas nouveau puisque vous venez de le saisir). trois fois déjà à l'étape 1 avant d'appuyer sur le bouton désagréable).
La logique derrière les liens de courrier électronique, cependant, mérite un examen minutieux, car laisser sa génération non sécurisée ouvre un flot de vulnérabilités concernant l'accès non autorisé aux comptes d'utilisateurs. Malheureusement, voici un exemple de structure d'URL de récupération basée sur l'UUID que beaucoup ont probablement rencontrée, mais qui ne respecte pas néanmoins les consignes de sécurité :
https://.../recover/d17ff6da-f5bf-11ee-9ce2-35a784c01695
Si un tel lien est utilisé, cela signifie généralement que n’importe qui peut obtenir votre mot de passe, et c’est aussi simple que cela. Cet article vise à approfondir les méthodes de génération d'UUID et à sélectionner des approches non sécurisées pour leur application.
L'UUID est une étiquette de 128 bits couramment utilisée pour générer des identifiants pseudo-aléatoires avec deux attributs précieux : elle est suffisamment complexe et suffisamment unique. Il s'agit principalement d'exigences clés pour que l'ID quitte le backend et soit montré explicitement à l'utilisateur dans le frontend ou généralement envoyé via l'API avec la possibilité d'être observé. Cela rend difficile la prédiction ou la force brute par rapport à id = 123 (complexité) et empêche les collisions lorsque l'identifiant généré est dupliqué sur un identifiant précédemment utilisé, par exemple un nombre aléatoire de 0 à 1000 (unicité).
Les parties « assez » proviennent en fait, premièrement, de certaines versions d'Universally Unique IDentifier, ce qui laisse la porte ouverte à des possibilités mineures de duplication, qui sont cependant facilement atténuées par une logique de comparaison supplémentaire et ne constituent pas une menace en raison de conditions difficilement contrôlées. son apparition. Et deuxièmement, la complexité des différentes versions d'UUID est décrite dans l'article, en général, elle est supposée être assez bonne, sauf dans d'autres cas particuliers.
Les clés primaires dans les tables de base de données semblent reposer sur les mêmes principes de complexité et d'unicité que l'UUID. Avec l'adoption généralisée de méthodes intégrées pour sa génération dans de nombreux langages de programmation et systèmes de gestion de bases de données, l'UUID constitue souvent le premier choix pour identifier les entrées de données stockées et comme champ pour joindre les tables en général et les sous-tables divisées par normalisation. L'envoi d'identifiants utilisateur provenant d'une base de données via une API en réponse à certaines actions est également une pratique courante pour simplifier le processus d'unification des flux de données sans génération d'identifiants temporaires supplémentaires et les relier à ceux du stockage de données de production.
En termes d'exemples de réinitialisation de mot de passe, l'architecture comprend plus probablement une table responsable d'une telle opération qui insère des lignes de données avec l'UUID généré chaque fois qu'un utilisateur clique sur le bouton. Il lance le processus de récupération en envoyant un e-mail à l'adresse associée à l'utilisateur par son user_id et en vérifiant pour quel utilisateur réinitialiser le mot de passe en fonction de l'identifiant dont il dispose une fois le lien de réinitialisation ouvert. Il existe cependant des directives de sécurité pour ces identifiants visibles par les utilisateurs, et certaines implémentations de l'UUID les respectent avec plus ou moins de succès.
La version 1 de la génération d'UUID divise ses 128 bits en utilisant une adresse MAC de 48 bits de l'identifiant de génération de périphérique, un horodatage de 60 bits, 14 bits stockés pour l'incrémentation de la valeur et 6 pour la gestion des versions. La garantie d'unicité est ainsi transférée des règles de la logique du code aux fabricants de matériel qui sont censés attribuer correctement des valeurs à chaque nouvelle machine en production. Ne laisser que 60+14 bits pour représenter la charge utile modifiable détériore l'intégrité de l'identifiant, en particulier avec une telle logique transparente derrière lui. Jetons un coup d'œil à une séquence de nombres d'UUID v1 générés en conséquence :
from uuid import uuid1 for _ in range(8): print(uuid1())
d17ff6da-f5bf-11ee-9ce2-35a784c01695 d17ff6db-f5bf-11ee-9ce2-35a784c01695 d17ff6dc-f5bf-11ee-9ce2-35a784c01695 d17ff6dd-f5bf-11ee-9ce2-35a784c01695 d17ff6de-f5bf-11ee-9ce2-35a784c01695 d17ff6df-f5bf-11ee-9ce2-35a784c01695 d17ff6e0-f5bf-11ee-9ce2-35a784c01695 d17ff6e1-f5bf-11ee-9ce2-35a784c01695
Comme on peut le voir, la partie "-f5bf-11ee-9ce2-35a784c01695" reste toujours la même. La partie modifiable est simplement une représentation hexadécimale de 16 bits de la séquence 3514824410 - 3514824417. Il s'agit d'un exemple superficiel car les valeurs de production sont généralement générées avec des écarts de temps plus importants entre les deux, de sorte que la partie liée à l'horodatage est également modifiée. La partie d'horodatage de 60 bits signifie également qu'une partie plus importante de l'identifiant est visuellement modifiée sur un plus grand échantillon d'identifiants. Le point central reste le même : l’UUIDv1 est facilement deviné, même si son aspect aléatoire apparaît initialement.
Prenez uniquement la première et la dernière valeur de la liste donnée de 8 identifiants. Comme les identifiants sont générés strictement, par conséquent, il est clair qu'il n'y a que 6 identifiants générés entre les deux donnés (en soustrayant les parties hexadécimales modifiables), et leurs valeurs peuvent également être définitivement trouvées. L'extrapolation d'une telle logique est la partie sous-jacente de l'attaque dite Sandwich visant à forcer brutalement l'UUID à connaître ces deux valeurs limites. Le flux d'attaque est simple : l'utilisateur génère l'UUID A avant que la génération de l'UUID cible ne se produise et l'UUID B juste après. En supposant que le même appareil avec une partie MAC statique de 48 bits soit responsable des trois générations, il définit un utilisateur avec une séquence d'identifiants potentiels entre A et B, où se trouve l'UUID cible. En fonction de la proximité temporelle entre les ID générés et la cible, la plage peut être en volumes accessibles à l'approche par force brute : vérifiez tous les UUID possibles pour trouver ceux existants parmi les vides.
Dans les requêtes API avec le point de terminaison de récupération de mot de passe décrit précédemment, cela se traduit par l'envoi de centaines ou de milliers de requêtes avec les UUID correspondants jusqu'à ce qu'une réponse indiquant l'URL existante soit trouvée. Avec la réinitialisation du mot de passe, cela conduit à une configuration dans laquelle l'utilisateur peut générer des liens de récupération sur deux comptes qu'il contrôle aussi étroitement que possible pour appuyer sur le bouton de récupération sur le compte cible auquel il n'a pas accès mais ne connaît que l'e-mail/la connexion. Les lettres aux comptes contrôlés avec les UUID de récupération A et B sont alors connues, et le lien cible pour récupérer le mot de passe du compte cible peut être forcé sans avoir accès à l'e-mail de réinitialisation réel.
La vulnérabilité provient du concept consistant à s'appuyer uniquement sur l'UUIDv1 pour l'authentification des utilisateurs. En envoyant un lien de récupération donnant accès à la réinitialisation des mots de passe, on suppose ainsi qu'en suivant le lien, un utilisateur s'authentifie comme celui qui était censé recevoir le lien. C'est la partie où la règle d'authentification échoue car l'UUIDv1 est exposé à une simple force brute de la même manière que si la porte de quelqu'un pouvait être ouverte en sachant à quoi ressemblent les clés des deux portes voisines.
La première version de l'UUID est principalement considérée comme héritée, en partie parce que la logique de génération n'utilise qu'une plus petite partie de la taille de l'identifiant comme valeur aléatoire. D'autres versions, comme la v4, tentent de résoudre ce problème en gardant le moins d'espace possible pour la gestion des versions et en laissant jusqu'à 122 bits comme charge utile aléatoire. En général, cela apporte le total des variations possibles à un 2^122
, qui pour l'instant est considéré comme satisfaisant à la partie "assez" concernant l'exigence d'unicité de l'identifiant et répond ainsi aux normes de sécurité. Une ouverture à une vulnérabilité par force brute peut apparaître si la mise en œuvre de la génération diminue de manière significative les bits restants pour la partie aléatoire. Mais sans outils de production ni bibliothèques, cela devrait-il être le cas ?
Adonnons-nous un peu à la cryptographie et examinons de près l'implémentation courante de la génération d'UUID par JavaScript. Voici la fonction randomUUID()
s'appuyant sur le module math.random
pour la génération de nombres pseudo-aléatoires :
Math.floor(Math.random()*0x10);
Et la fonction aléatoire elle-même, pour faire court, n'est que la partie qui intéresse le sujet de cet article :
hi = 36969 * (hi & 0xFFFF) + (hi >> 16); lo = 18273 * (lo & 0xFFFF) + (lo >> 16); return ((hi << 16) + (lo & 0xFFFF)) / Math.pow(2, 32);
La génération pseudo-aléatoire nécessite une valeur de départ comme base pour effectuer des opérations mathématiques dessus afin de produire des séquences de nombres suffisamment aléatoires. De telles fonctions sont uniquement basées sur celui-ci, ce qui signifie que si elles sont réinitialisées avec la même graine qu'auparavant, la séquence de sortie correspondra. La valeur de départ dans la fonction JavaScript en question comprend des variables hi et lo, chacune étant un entier non signé de 32 bits (0 à 4294967295 décimal). Une combinaison des deux est nécessaire à des fins cryptographiques, ce qui rend presque impossible l’inversion définitive des deux valeurs initiales en connaissant leur multiple, car cela repose sur la complexité de la factorisation d’entiers avec de grands nombres.
Deux entiers de 32 bits rassemblent 2^64
cas possibles pour deviner les variables hi et lo derrière la fonction initialisée produisant des UUID. Si les valeurs hi et lo sont connues d'une manière ou d'une autre, il ne faut aucun effort pour dupliquer la fonction de génération et connaître toutes les valeurs qu'elle produit et produira dans le futur en raison de l'exposition de la valeur de départ. Cependant, le 64 bits des normes de sécurité peut être considéré comme intolérant à la force brute sur une période de temps mesurable pour que cela ait un sens. Comme toujours, le problème vient d’une mise en œuvre spécifique. Math.random()
prend divers 16 bits de chacun des hi et lo en résultats 32 bits ; cependant, randomUUID()
au-dessus décale à nouveau la valeur en raison de l'opération .floor()
, et la seule partie significative vient tout d'un coup exclusivement de hi. Cela n'affecte en rien la génération, mais provoque l'effondrement des approches de cryptographie car il ne laisse que 2^32
combinaisons possibles pour l'ensemble de la fonction de génération (il n'est pas nécessaire de forcer brutalement hi et lo car lo peut être défini sur n'importe quel valeur et n’influence pas la sortie).
Le flux par force brute consiste à acquérir un identifiant unique et à tester les valeurs élevées possibles qui auraient pu le générer. Avec une certaine optimisation et un matériel informatique moyen, cela peut prendre seulement quelques minutes et ne nécessite pas d'envoyer de nombreuses requêtes au serveur comme dans l'attaque Sandwich, mais effectue plutôt toutes les opérations hors ligne. Le résultat d'une telle approche provoque la réplication de l'état de la fonction de génération utilisé dans le backend pour obtenir tous les liens créés et futurs réinitialisés dans l'exemple de récupération de mot de passe. Les étapes pour empêcher l'émergence d'une vulnérabilité sont simples et nécessitent l'utilisation de fonctions cryptographiquement sécurisées, par exemple crypto.randomUUID()
.
L'UUID est un concept génial qui facilite grandement la vie des ingénieurs de données dans de nombreux domaines d'application. Cependant, il ne doit jamais être utilisé en relation avec l'authentification, car dans cet article, des failles dans certains cas de ses techniques de génération sont mises en lumière. Cela ne signifie évidemment pas que tous les UUID ne sont pas sécurisés. L'approche de base, cependant, consiste à persuader les gens de ne pas les utiliser du tout pour des raisons de sécurité, ce qui est plus efficace et plus sûr que de fixer des limites complexes dans la documentation sur les utilisations ou sur la manière de ne pas les générer à cette fin.