Estrategia para generar identificadores únicos y seguros para usar en una aplicación web "a veces fuera de línea"

47

Tengo un proyecto basado en la web que permite a los usuarios trabajar tanto en línea como fuera de línea y estoy buscando una forma de generar identificadores únicos para registros en el lado del cliente. Me gustaría un enfoque que funcione mientras un usuario está fuera de línea (es decir, no puede hablar con un servidor), se garantiza que sea único y seguro. Por "seguro", me preocupa específicamente que los clientes envíen identificaciones duplicadas (de forma maliciosa o de otro tipo) y, por lo tanto, causen estragos en la integridad de los datos.

He estado buscando en Google, esperando que esto ya fuera un problema resuelto. No he encontrado nada que sea muy definitivo, especialmente en términos de enfoques que se utilizan en los sistemas de producción. Encontré algunos ejemplos para sistemas donde los usuarios solo accederán a los datos que han creado (por ejemplo, una lista de Todo a la que se accede en múltiples dispositivos, pero solo por el usuario que la creó). Desafortunadamente, necesito algo un poco más sofisticado. Encontré algunas ideas realmente buenas aquí , que están en línea con la forma en que pensaba que las cosas podrían funcionar.

A continuación se muestra mi solución propuesta.

Algunos requisitos

  1. Las ID deben ser globalmente únicas (o al menos únicas dentro del sistema)
  2. Generado en el cliente (es decir, a través de JavaScript en el navegador)
  3. Seguro (como se describe anteriormente y de lo contrario)
  4. Los datos pueden ser vistos / editados por múltiples usuarios, incluidos los usuarios que no los crearon
  5. No causa problemas de rendimiento importantes para los backend db's (como MongoDB o CouchDB)

Solución propuesta

Cuando los usuarios crean una cuenta, recibirán un uuid que fue generado por el servidor y que se sabe que es único dentro del sistema. Este ID NO debe ser el mismo que el token de autenticación de usuarios. Llamemos a esta identificación los usuarios "token de identificación".

Cuando un usuario crea un nuevo registro, genera un nuevo uuid en javascript (generado usando window.crypto cuando está disponible. Vea ejemplos aquí ). Esta identificación se concatena con el "token de identificación" que recibió el usuario cuando creó su cuenta. Esta nueva identificación compuesta (token de identificación del lado del servidor + uuid del lado del cliente) ahora es el identificador único para el registro. Cuando el usuario está en línea y envía este nuevo registro al servidor de fondo, el servidor:

  1. Identifique esto como una acción de "inserción" (es decir, no una actualización o una eliminación)
  2. Validar ambas partes de la clave compuesta son uuids válidos
  3. Valide que la parte de "token de identificación" proporcionada de la identificación compuesta es correcta para el usuario actual (es decir, coincide con la ficha de identificación que el servidor asignó al usuario cuando creó su cuenta)
  4. Si todo está copasetic, insertar los datos en la base de datos (teniendo cuidado de hacer una inserción y no un "upsert" de modo que si el id hace ya existe no se actualiza un registro existente por error)

Las consultas, actualizaciones y eliminaciones no requerirían ninguna lógica especial. Simplemente usarían la identificación para el registro de la misma manera que las aplicaciones tradicionales.

¿Cuáles son las ventajas de este enfoque?

  1. El código del cliente puede crear nuevos datos sin conexión y conocer la identificación de ese registro de inmediato. Consideré enfoques alternativos donde se generaría una identificación temporal en el cliente que luego se cambiaría por una identificación "final" cuando el sistema estaba en línea. Sin embargo, esto se sintió muy frágil. Especialmente cuando empiezas a pensar en crear datos secundarios con claves foráneas que también deberían actualizarse. Sin mencionar tratar con URL que cambiarían cuando cambiara la identificación.

  2. Al hacer que los identificadores sean un compuesto de un valor generado por el cliente Y un valor generado por el servidor, cada usuario está creando efectivamente identificadores en un entorno limitado. Esto tiene la intención de limitar el daño que puede hacer un cliente malicioso / deshonesto. Además, cualquier colisión de identificación es por usuario, no global para todo el sistema.

  3. Dado que el token de identificación de un usuario está vinculado a su cuenta, los identificadores solo pueden generar identificadores en un entorno limitado de usuarios autenticados (es decir, cuando el usuario inició sesión correctamente). Esto está destinado a evitar que los clientes malintencionados creen identificadores incorrectos para un usuario. Por supuesto, si un token de autenticación de usuarios fue robado por un cliente malintencionado, podrían hacer cosas malas. Pero, una vez que se ha robado un token de autenticación, la cuenta se ve comprometida de todos modos. En el caso de que esto sucediera, el daño causado se limitaría a la cuenta comprometida (no a todo el sistema).

Preocupaciones

Estas son algunas de mis preocupaciones con este enfoque.

  1. ¿Esto generará identificadores suficientemente únicos para una aplicación a gran escala? ¿Hay alguna razón para pensar que esto provocará colisiones de identificación? ¿Puede JavaScript generar un uuid suficientemente aleatorio para que esto funcione? Parece que window.crypto está bastante disponible y este proyecto ya requiere navegadores razonablemente modernos. ( esta pregunta ahora tiene una pregunta SO por separado )

  2. ¿Me faltan algunas lagunas que podrían permitir que un usuario malintencionado comprometa el sistema?

  3. ¿Hay alguna razón para preocuparse por el rendimiento de la base de datos al consultar una clave compuesta compuesta por 2 uuids? ¿Cómo se debe almacenar esta identificación para un mejor rendimiento? ¿Dos campos separados o un solo campo de objeto? ¿Habría un "mejor" enfoque diferente para Mongo vs Couch? Sé que tener una clave primaria no secuencial puede causar problemas notables de rendimiento al hacer inserciones. ¿Sería más inteligente tener un valor generado automáticamente para la clave primaria y almacenar esta identificación como un campo separado? ( esta pregunta ahora tiene una pregunta SO por separado )

  4. Con esta estrategia, sería fácil determinar que un mismo conjunto de registros fue creado por el mismo usuario (ya que todos compartirían el mismo token de identificación visible públicamente). Si bien no veo ningún problema inmediato con esto, siempre es mejor no filtrar más información sobre detalles internos de la necesaria. Otra posibilidad sería hacer un hash de la clave compuesta, pero parece que puede ser más problemático de lo que vale.

  5. En el caso de que haya una colisión de id para un usuario, no hay una manera simple de recuperarse. Supongo que el cliente podría generar una nueva identificación, pero esto parece mucho trabajo para un caso límite que realmente nunca debería suceder. Tengo la intención de dejar esto sin abordar.

  6. Solo los usuarios autenticados pueden ver y / o editar datos. Esta es una limitación aceptable para mi sistema.

Conclusión

¿Está por encima de un plan razonable? Me doy cuenta de que parte de esto se reduce a una llamada de juicio basada en una comprensión más completa de la aplicación en cuestión.

Herbrandson
fuente
Creo que esta pregunta puede interesarle stackoverflow.com/questions/105034/... También esto me leyó como GUID pero no parecen ser nativos en javascript
Rémi
2
Me sorprende que los UUID ya satisfagan los 5 requisitos enumerados. ¿Por qué son insuficientes?
Gabe
@Gabe Vea mis comentarios en la publicación de Lie Ryans a continuación
herbrandson
discusión meta de esta pregunta: meta.stackoverflow.com/questions/251215/...
mosquito
"cliente malicioso / rouge" - pícaro.
David Conrad

Respuestas:

4

Tu enfoque funcionará. Muchos sistemas de gestión de documentos utilizan este tipo de enfoque.

Una cosa a tener en cuenta es que no necesita usar tanto el uuid del usuario como la identificación del elemento aleatorio como parte de la cadena. En su lugar, puede hacer hash la concatenación de ambos. Esto le dará un identificador más corto, y posiblemente otros beneficios, ya que los ID resultantes se distribuirán de manera más uniforme (mejor equilibrado para la indexación y almacenamiento de archivos si está almacenando archivos en función de su uuid).

Otra opción que tiene es generar solo un uuid temporal para cada elemento. Luego, cuando se conecta y los publica en el servidor, el servidor genera uuid (garantizado) para cada elemento y se lo devuelve. Luego actualiza su copia local.

Gran maestro B
fuente
2
Había considerado usar un hash de los 2 como id. Sin embargo, no me pareció que hubiera una forma adecuada de generar un sha256 en todos los navegadores que necesito admitir :(
herbrandson
12

Debe separar las dos preocupaciones:

  1. Generación de ID: el cliente debe poder generar un identificador único en el sistema distribuido
  2. Problema de seguridad: el cliente DEBE tener un token de autenticación de usuario válido Y el token de autenticación es válido para el objeto que se crea / modifica

La solución a estos dos, lamentablemente, está separada; pero afortunadamente no son incompatibles.

La preocupación sobre la generación de ID se resuelve fácilmente generando con UUID, para eso está diseñado UUID; Sin embargo, la preocupación de seguridad requeriría que verifique en el servidor que el token de autenticación dado está autorizado para la operación (es decir, si el token de autenticación es para un usuario que no posee el permiso necesario sobre ese objeto en particular, entonces DEBE ser rechazado).

Cuando se maneja correctamente, la colisión realmente no representaría un problema de seguridad (simplemente se le pedirá al usuario o al cliente que vuelva a intentar la operación con otro UUID).

Lie Ryan
fuente
Este es un muy buen punto. Quizás eso es todo lo que se requiere y lo estoy pensando demasiado. Sin embargo, tengo algunas preocupaciones sobre este enfoque. Lo más importante es que los uuids generados por JavaScript no parecen ser tan aleatorios como uno podría esperar (ver los comentarios en stackoverflow.com/a/2117523/13181 para las detenciones). Parece que window.crypto debería resolver este problema, pero parece que no está disponible en todas las versiones de navegador que necesito admitir.
herbrandson
continúa ... Me gusta su sugerencia de agregar código en el cliente que regenere un nuevo uuid en el caso de una colisión. Sin embargo, me parece que esto reintroduce la preocupación que tenía en mi publicación en el punto 1 de la sección "¿Cuáles son las ventajas de este enfoque?". Creo que si seguía esa ruta, sería mejor generar identificaciones temporales en el lado del cliente y luego actualizarlas con la "identificación final" desde el servidor una vez conectado
herbrandson
continuó de nuevo ... Además, permitir que los usuarios envíen sus propias identificaciones únicas parece una preocupación de seguridad. Quizás el tamaño de un líquido y la alta improbabilidad estadística de una colisión son suficientes para mitigar esta preocupación en sí mismos. No estoy seguro. Tengo una sospecha persistente de que mantener a cada usuario en su propia "caja de arena" es solo una buena idea en este caso (es decir, no confiar en la entrada del usuario).
herbrandson
@herbrandson: No se me ocurre ningún problema de seguridad al permitir que los usuarios generen su propia identificación única, siempre y cuando siempre verifique que el usuario tenga los permisos para la operación. La identificación es solo algo que se puede usar para identificar objetos, realmente no importa cuál sea su valor. El único daño potencial es que el usuario puede reservar un rango de ID para su propio uso, pero eso realmente no plantea ningún problema para el sistema en su conjunto porque es poco probable que otros usuarios lleguen a esos valores solo por casualidad.
Lie Ryan
Gracias por sus comentarios. ¡Realmente me obligó a aclarar mi pensamiento! Hay una razón por la que desconfiaba de su enfoque, y lo había olvidado en el camino :). Mi miedo está ligado al pobre RNG en muchos navegadores. Para la generación de líquidos, uno preferiría un RNG criptográficamente fuerte. Muchos navegadores más nuevos tienen esto a través de window.crypto, pero los navegadores más antiguos no. Debido a esto, es posible que un usuario malintencionado descubra la semilla de otros usuarios de RNG y, por lo tanto, conozca el próximo uuid que se generará. Esta es la parte que parece que podría ser atacada.
herbrandson