Difícilmente hay una persona hoy en día que nunca haya hecho clic en el botón "Recuperar contraseña" con profunda frustración. Incluso si parece que la contraseña era sin duda correcta, el siguiente paso para recuperarla se realiza sin problemas visitando un enlace de un correo electrónico e ingresando la nueva contraseña (no engañemos a nadie; no es nada nuevo ya que acaba de escribirla). tres veces ya en el paso 1 antes de presionar el botón desagradable).
Sin embargo, la lógica detrás de los enlaces de correo electrónico es algo que merece un gran escrutinio, ya que dejar a su generación insegura abre una avalancha de vulnerabilidades relacionadas con el acceso no autorizado a cuentas de usuarios. Desafortunadamente, aquí hay un ejemplo de una estructura de URL de recuperación basada en UUID que muchos probablemente encontraron y que, sin embargo, no sigue las pautas de seguridad:
https://.../recover/d17ff6da-f5bf-11ee-9ce2-35a784c01695
Si se utiliza dicho enlace, generalmente significa que cualquiera puede obtener su contraseña, y es así de simple. Este artículo tiene como objetivo profundizar en los métodos de generación de UUID y seleccionar enfoques inseguros para su aplicación.
UUID es una etiqueta de 128 bits comúnmente utilizada para generar identificadores pseudoaleatorios con dos atributos valiosos: es lo suficientemente compleja y única. En su mayoría, esos son requisitos clave para que la identificación salga del backend y se muestre al usuario explícitamente en el frontend o, generalmente, se envíe a través de API con la capacidad de ser observada. Hace que uno sea difícil de adivinar o de fuerza bruta en comparación con id = 123 (complejidad) y evita colisiones cuando la identificación generada se duplica con la utilizada anteriormente, por ejemplo, un número aleatorio de 0 a 1000 (unicidad).
Las piezas "suficientes" en realidad provienen, en primer lugar, de algunas versiones de Universally Unique IDentifier, lo que deja abierto a pequeñas posibilidades de duplicación, lo que sin embargo se puede mitigar fácilmente mediante una lógica de comparación adicional y no representa una amenaza debido a condiciones de uso apenas controladas. su ocurrencia. Y en segundo lugar, la complejidad de varias versiones de UUID se describe en el artículo; en general, se supone que es bastante buena, excepto en otros casos extremos.
Las claves primarias en las tablas de bases de datos parecen depender de los mismos principios de ser complejas y únicas que el UUID. Con la amplia adopción de métodos integrados para su generación en muchos lenguajes de programación y sistemas de administración de bases de datos, UUID a menudo se presenta como la primera opción para identificar entradas de datos almacenadas y como un campo para unir tablas en general y subtablas divididas por normalización. Enviar ID de usuario que provienen de una base de datos a través de API en respuesta a ciertas acciones también es una práctica común para simplificar el proceso de unificar flujos de datos sin generar ID temporales adicionales y vincularlos a los que se encuentran en el almacenamiento de datos de producción.
En términos de ejemplos de restablecimiento de contraseña, lo más probable es que la arquitectura incluya una tabla responsable de dicha operación que inserta filas de datos con el UUID generado cada vez que un usuario hace clic en el botón. Inicia el proceso de recuperación enviando un correo electrónico a la dirección asociada con el usuario por su user_id y verificando para qué usuario restablecer la contraseña según el identificador que tiene una vez que se abre el enlace de restablecimiento. Sin embargo, existen pautas de seguridad para dichos identificadores visibles para los usuarios, y ciertas implementaciones de UUID las cumplen con distintos grados de éxito.
La versión 1 de la generación de UUID divide sus 128 bits usando una dirección MAC de 48 bits del dispositivo que genera el identificador, una marca de tiempo de 60 bits, 14 bits almacenados para incrementar el valor y 6 para el control de versiones. La garantía de unicidad se transfiere así de las reglas de la lógica del código a los fabricantes de hardware, quienes deben asignar correctamente los valores a cada nueva máquina en producción. Dejar solo 60+14 bits para representar una carga útil modificable deteriora la integridad del identificador, especialmente con una lógica tan transparente detrás. Echemos un vistazo a una secuencia del número de UUID v1 generado en consecuencia:
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
Como puede verse, la parte "-f5bf-11ee-9ce2-35a784c01695" permanece igual todo el tiempo. La parte modificable es simplemente una representación hexadecimal de 16 bits de la secuencia 3514824410 - 3514824417. Es un ejemplo superficial, ya que los valores de producción generalmente se generan con intervalos de tiempo más significativos entre ellos, por lo que la parte relacionada con la marca de tiempo también se cambia. La parte de la marca de tiempo de 60 bits también significa que una parte más significativa del identificador se cambia visualmente en una muestra más grande de ID. El punto central sigue siendo el mismo: UUIDv1 se adivina fácilmente, por muy aleatorio que parezca inicialmente.
Tome solo el primer y último valor de la lista dada de 8 identificadores. Como los identificadores se generan estrictamente, en consecuencia, está claro que solo se generan 6 ID entre los dos dados (restando las partes cambiables hexadecimales), y sus valores también se pueden encontrar definitivamente. La extrapolación de dicha lógica es la parte subyacente detrás del llamado ataque Sandwich que tiene como objetivo forzar a UUID a desconocer estos dos valores fronterizos. El flujo de ataque es sencillo: el usuario genera el UUID A antes de que se produzca la generación del UUID objetivo y el UUID B inmediatamente después. Suponiendo que el mismo dispositivo con una parte MAC estática de 48 bits es responsable de las tres generaciones, configura un usuario con una secuencia de ID potenciales entre A y B, donde se encuentra el UUID de destino. Dependiendo de la proximidad temporal entre los ID generados y el objetivo, el rango puede estar en volúmenes accesibles al enfoque de fuerza bruta: verifique todos los UUID posibles para encontrar los existentes entre los vacíos.
En las solicitudes de API con el punto final de recuperación de contraseña descrito anteriormente, se traduce en enviar cientos o miles de solicitudes con los UUID consiguientes hasta que se encuentre una respuesta que indique la URL existente. Con el restablecimiento de contraseña, conduce a una configuración en la que el usuario puede generar enlaces de recuperación en dos cuentas que controla lo más estrechamente posible para presionar el botón de recuperación en la cuenta de destino a la que no tiene acceso pero que solo conoce el correo electrónico/el inicio de sesión. Entonces se conocen las cartas enviadas a cuentas controladas con UUID de recuperación A y B, y el enlace de destino para recuperar la contraseña de la cuenta de destino se puede forzar de forma bruta sin tener acceso al correo electrónico de restablecimiento real.
La vulnerabilidad se origina en el concepto de confiar únicamente en UUIDv1 para la autenticación del usuario. Al enviar un enlace de recuperación que otorga acceso para restablecer contraseñas, se supone que al seguir el enlace, el usuario se autentica como el que se suponía que debía recibir el enlace. Esta es la parte donde la regla de autenticación falla debido a que UUIDv1 está expuesto a fuerza bruta directa de la misma manera que si la puerta de alguien pudiera abrirse sabiendo cómo son las llaves de las dos puertas vecinas.
La primera versión de UUID se considera principalmente heredada en parte porque la lógica de generación solo utiliza una porción más pequeña del tamaño del identificador como valor aleatorio. Otras versiones, como la v4, intentan resolver este problema manteniendo el menor espacio posible para el control de versiones y dejando hasta 122 bits para la carga útil aleatoria. En general, trae todas las variaciones posibles a un whooping 2^122
, que por ahora se considera que satisface la parte "suficiente" con respecto al requisito de unicidad del identificador y, por lo tanto, cumple con los estándares de seguridad. Podría aparecer una vulnerabilidad de fuerza bruta si la implementación de la generación de alguna manera disminuye significativamente los bits que quedan para la parte aleatoria. Pero sin herramientas de producción ni bibliotecas, ¿debería ser así?
Entremos un poco en la criptografía y echemos un vistazo de cerca a la implementación común de generación de UUID de JavaScript. Aquí está la función randomUUID()
que se basa en el módulo math.random
para la generación de números pseudoaleatorios:
Math.floor(Math.random()*0x10);
Y la función aleatoria en sí, para abreviar, es solo la parte de interés del tema de este artículo:
hi = 36969 * (hi & 0xFFFF) + (hi >> 16); lo = 18273 * (lo & 0xFFFF) + (lo >> 16); return ((hi << 16) + (lo & 0xFFFF)) / Math.pow(2, 32);
La generación pseudoaleatoria requiere un valor inicial como base para realizar operaciones matemáticas sobre él para producir secuencias de números suficientemente aleatorios. Dichas funciones se basan únicamente en él, lo que significa que si se reinician con la misma semilla que antes, la secuencia de salida coincidirá. El valor inicial en la función JavaScript en cuestión comprende las variables hi y lo, cada una de las cuales es un entero sin signo de 32 bits (0 a 4294967295 decimal). Se necesita una combinación de ambos para fines criptográficos, lo que hace casi imposible revertir definitivamente los dos valores iniciales conociendo su múltiplo, ya que depende de la complejidad de la factorización de enteros con números grandes.
Dos enteros de 32 bits juntos generan 2^64
casos posibles para adivinar variables altas y bajas detrás de la función inicializada que produce UUID. Si los valores hi y lo se conocen de alguna manera, no requiere ningún esfuerzo duplicar la función de generación y conocer todos los valores que produce y producirá en el futuro debido a la exposición del valor inicial. Sin embargo, los 64 bits en los estándares de seguridad pueden considerarse intolerantes a la fuerza bruta en un período de tiempo mensurable para que tenga sentido. Como siempre, el problema surge de una implementación específica. Math.random()
toma varios 16 bits de cada uno de hi y lo en resultados de 32 bits; sin embargo, randomUUID()
encima cambia el valor una vez más debido a la operación .floor()
, y de repente la única parte significativa ahora proviene exclusivamente de hi. No afecta la generación de ninguna manera, pero hace que los enfoques de criptografía se desmoronen, ya que solo deja 2^32
combinaciones posibles para toda la semilla de la función de generación (no hay necesidad de aplicar fuerza bruta tanto a hi como a lo, ya que lo se puede configurar en cualquier valor y no influye en la salida).
El flujo de fuerza bruta consiste en adquirir una única ID y probar posibles valores altos que podrían haberla generado. Con algo de optimización y hardware promedio de una computadora portátil, puede tomar solo un par de minutos y no requiere enviar muchas solicitudes al servidor como en el ataque Sandwich, sino que realiza todas las operaciones sin conexión. El resultado de este enfoque provoca la replicación del estado de la función de generación utilizada en el backend para obtener todos los enlaces creados y de restablecimiento futuro en el ejemplo de recuperación de contraseña. Los pasos para evitar que surja una vulnerabilidad son sencillos y recomiendan el uso de funciones criptográficamente seguras, por ejemplo, crypto.randomUUID()
.
UUID es un gran concepto y facilita mucho la vida de los ingenieros de datos en muchas áreas de aplicaciones. Sin embargo, nunca debe utilizarse en relación con la autenticación, ya que en este artículo se sacan a la luz fallas en ciertos casos en sus técnicas de generación. Obviamente, esto no se traduce en la idea de que todos los UUID sean inseguros. Sin embargo, el enfoque básico es persuadir a las personas para que no los utilicen en absoluto por motivos de seguridad, lo cual es más eficiente y, bueno, seguro que establecer límites complejos en la documentación sobre qué usarlos o cómo no generarlos para tal fin.