From 07993ffb91aa77e1f0f0527588504f32f7579730 Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 28 Jul 2026 00:35:11 +0200 Subject: [PATCH] staging: neutralizar credenciales OAuth vivas, nombres reales y tokens de sesion La primera version solo invalidaba emails, fireBaseToken, campos telegram* y services.google.email. Auditando el staging aparecieron, sin neutralizar: - 42 users con services.google.accessToken/refreshToken/idToken -> credenciales OAuth VIVAS que dan acceso a la cuenta de Google REAL del usuario - 42 users con services.google.name/given_name/family_name/picture - 99 users con profile.name.first/last (nombre y apellidos reales) - tokens de sesion (services.resume.loginTokens) y verificationTokens Pasaron desapercibidos porque el bloque de verificacion tampoco los comprobaba. Se anaden los $unset y se amplia la verificacion para que falle si reaparecen. services.password.bcrypt se CONSERVA a proposito (hashes, no reversibles directamente, y son la unica forma de hacer QA de login: el volcado real no trae la cuenta de fixtures). Documentado en el propio script. --- scripts/neutralize-contacts.mongo.js | 74 ++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/scripts/neutralize-contacts.mongo.js b/scripts/neutralize-contacts.mongo.js index 9732ed1..ad7f04c 100644 --- a/scripts/neutralize-contacts.mongo.js +++ b/scripts/neutralize-contacts.mongo.js @@ -58,6 +58,47 @@ const rGoogle = dbx.users.updateMany( [{ $set: { 'services.google.email': { $concat: [{ $toString: '$_id' }, '@invalid.test'] } } }] ); +// 3b) users.services.google: purgar CREDENCIALES VIVAS y nombre/foto reales. +// accessToken/refreshToken/idToken son credenciales OAuth que dan acceso a la cuenta +// de Google REAL del usuario — lo más grave del volcado. name/given_name/family_name/ +// picture/id son PII directa. (El paso 3 solo invalidaba el email.) +const rGoogleCreds = dbx.users.updateMany( + { 'services.google': { $exists: true } }, + { $unset: { + 'services.google.accessToken': '', + 'services.google.refreshToken': '', + 'services.google.idToken': '', + 'services.google.expiresAt': '', + 'services.google.scope': '', + 'services.google.id': '', + 'services.google.name': '', + 'services.google.given_name': '', + 'services.google.family_name': '', + 'services.google.picture': '', + 'services.google.locale': '', + 'services.google.gender': '' + }} +); + +// 3c) users.profile.name -> nombres y apellidos REALES (first/last). Se vacía el subdoc. +const rProfile = dbx.users.updateMany( + { 'profile.name': { $exists: true } }, + { $unset: { 'profile.name.first': '', 'profile.name.last': '', 'profile.email': '' } } +); + +// 3d) users.services.resume.loginTokens -> tokens de SESIÓN vivos (permiten suplantar +// al usuario sin contraseña). Se vacía el array. Idem verificationTokens de email. +const rTokens = dbx.users.updateMany( + { $or: [ { 'services.resume.loginTokens.0': { $exists: true } }, + { 'services.email.verificationTokens.0': { $exists: true } } ] }, + { $set: { 'services.resume.loginTokens': [], 'services.email.verificationTokens': [] } } +); + +// NOTA (decisión consciente, NO es un olvido): `services.password.bcrypt` se CONSERVA. +// Son hashes bcrypt (no reversibles directamente) y son la única forma de hacer QA de +// login en staging — no hay cuenta de fixtures en el volcado real. Si se quisiera cerrar +// también ese vector, hay que crear antes una cuenta de pruebas dedicada. + // 4) subscriptions: quitar el destino de Telegram (chatId) de las subs de ese tipo const rSubs = dbx.subscriptions.updateMany( { chatId: { $exists: true } }, { $unset: { chatId: '' } } @@ -75,6 +116,9 @@ printjson({ usersEmailsNeutralizados: rEmails.modifiedCount, usersTokensPurgados: rUserTokens.modifiedCount, googleEmailsInvalidados: rGoogle.modifiedCount, + googleCredsPurgadas: rGoogleCreds.modifiedCount, + profileNombresPurgados: rProfile.modifiedCount, + tokensSesionPurgados: rTokens.modifiedCount, subsChatIdEliminados: rSubs.modifiedCount, colaCorreoBorrada: rMailQueue.deletedCount, colaJobsBorrada: rMailJobs.deletedCount @@ -87,9 +131,31 @@ const emailMalo = dbx.users.countDocuments({ 'emails.address': { $exists: true const googleMalo = dbx.users.countDocuments({ 'services.google.email': { $exists: true, $not: /@invalid\.test$/ } }); const subsConChat = dbx.subscriptions.countDocuments({ chatId: { $exists: true } }); const colaCorreo = dbx.getCollection('__mailTimeQueue__').countDocuments({}); -printjson({ verificacion: { usersConFireBaseToken: conToken, usersConEmailRealVisible: emailMalo, usersConGoogleEmailReal: googleMalo, subsConChatId: subsConChat, correosEnCola: colaCorreo } }); -if (conToken === 0 && emailMalo === 0 && googleMalo === 0 && subsConChat === 0 && colaCorreo === 0) { - print('OK: contactos neutralizados. Ninguna notificación puede alcanzar a un usuario real.'); +// Campos que la primera versión del script NO comprobaba — por eso la fuga pasó +// desapercibida (42 users con credenciales OAuth vivas + 99 con nombre real). +const googleCreds = dbx.users.countDocuments({ $or: [ + { 'services.google.accessToken': { $exists: true } }, + { 'services.google.refreshToken': { $exists: true } }, + { 'services.google.idToken': { $exists: true } } +] }); +const googleNom = dbx.users.countDocuments({ 'services.google.name': { $exists: true } }); +const profileNom = dbx.users.countDocuments({ $or: [ + { 'profile.name.first': { $exists: true } }, + { 'profile.name.last': { $exists: true } } +] }); +const sesiones = dbx.users.countDocuments({ 'services.resume.loginTokens.0': { $exists: true } }); +printjson({ verificacion: { + usersConFireBaseToken: conToken, usersConEmailRealVisible: emailMalo, + usersConGoogleEmailReal: googleMalo, usersConGoogleCredsVivas: googleCreds, + usersConGoogleNombreReal: googleNom, usersConProfileNombreReal: profileNom, + usersConTokenSesion: sesiones, subsConChatId: subsConChat, correosEnCola: colaCorreo +} }); +const pendientes = conToken + emailMalo + googleMalo + googleCreds + googleNom + + profileNom + sesiones + subsConChat + colaCorreo; +if (pendientes === 0) { + print('OK: contactos neutralizados. Ninguna notificación puede alcanzar a un usuario real,'); + print(' y no quedan credenciales OAuth/sesión ni nombres reales.'); + print('NOTA: services.password.bcrypt se conserva a propósito (QA de login) — ver comentario arriba.'); } else { - print('AVISO: quedan campos sin neutralizar — revisar antes de exponer el staging.'); + print('AVISO: quedan ' + pendientes + ' campos sin neutralizar — revisar antes de exponer el staging.'); }