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.
This commit is contained in:
vjrj 2026-07-28 00:35:11 +02:00
parent 2e298f9ee5
commit 07993ffb91

View file

@ -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.');
}