Pentester-Hinweis: bcrypt-Hash (Login) und verschluesseltes Reveal-Feld
koennen out-of-sync sein -> Support liest ein Passwort vor, das beim
Login scheitert.
Analyse: alle aktuellen Schreibpfade sind konsistent (beide Felder
zusammen, oder encrypted=null, oder Rehash desselben Passworts) - der
Code erzeugt keinen Desync. Ursache = Altlast/manueller DB-Eingriff.
Fix (defensiv, unabhaengig von der Ursache):
- getCustomerPortalPassword liefert {status: ok|none|desync} und prueft
den entschluesselten Klartext per bcrypt.compare gegen den Login-Hash.
- Bei desync (oder Entschluesselungsfehler) geben WEDER Reveal NOCH
Send-Credentials das Passwort aus -> 409 'Dateninkonsistenz, bitte
neu setzen'. Reveal-Read wird mit Status auditiert.
- Neues Diagnose-Script scripts/check-portal-password-sync.ts scannt
alle Portal-Kunden auf Desync (nur Diagnose, aendert nichts) - fuer
Prod, da der Pentester keinen FS-Zugriff hat.
Verifiziert: desync -> nicht ausgegeben; konsistent -> ok; kein PW ->
none. Scan laeuft (0 Desync auf Dev).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
2.0 KiB
TypeScript
62 lines
2.0 KiB
TypeScript
/**
|
||
* Diagnose: findet Portal-Kunden, deren gespeichertes (verschlüsseltes)
|
||
* Passwort NICHT zum Login-Hash (bcrypt) passt. Solche „Desyncs" führen dazu,
|
||
* dass der Reveal/Send-Credentials ein Passwort ausgibt, das beim Login
|
||
* scheitert. Seit dem Reveal-Konsistenzcheck werden sie nicht mehr ausgegeben –
|
||
* dieses Script findet bestehende Altlasten (z.B. aus manuellen DB-Eingriffen
|
||
* oder alten Code-Versionen), damit man sie gezielt neu setzen kann.
|
||
*
|
||
* Aufruf (im Backend-Container/-Verzeichnis):
|
||
* npx tsx scripts/check-portal-password-sync.ts
|
||
* Nur Diagnose – ändert NICHTS.
|
||
*/
|
||
import { PrismaClient } from '@prisma/client';
|
||
import bcrypt from 'bcryptjs';
|
||
import { decrypt } from '../src/utils/encryption.js';
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
async function main() {
|
||
const customers = await prisma.customer.findMany({
|
||
where: { portalPasswordEncrypted: { not: null }, portalPasswordHash: { not: null } },
|
||
select: {
|
||
id: true,
|
||
customerNumber: true,
|
||
portalEmail: true,
|
||
portalPasswordEncrypted: true,
|
||
portalPasswordHash: true,
|
||
},
|
||
});
|
||
|
||
let desync = 0;
|
||
for (const c of customers) {
|
||
let decrypted: string;
|
||
try {
|
||
decrypted = decrypt(c.portalPasswordEncrypted!);
|
||
} catch {
|
||
desync++;
|
||
console.log(`DESYNC (Entschlüsselung fehlgeschlagen): Kunde #${c.id} ${c.customerNumber}`);
|
||
continue;
|
||
}
|
||
const matches = await bcrypt.compare(decrypted, c.portalPasswordHash!);
|
||
if (!matches) {
|
||
desync++;
|
||
console.log(`DESYNC: Kunde #${c.id} ${c.customerNumber} (${c.portalEmail ?? '-'}) – gespeichertes Passwort passt nicht zum Login-Hash`);
|
||
}
|
||
}
|
||
|
||
console.log(`\nGeprüft: ${customers.length} Portal-Kunden mit beiden Feldern. Desync: ${desync}.`);
|
||
if (desync > 0) {
|
||
console.log('→ Für diese Kunden bitte ein neues Portal-Passwort setzen/generieren.');
|
||
} else {
|
||
console.log('→ Alles konsistent.');
|
||
}
|
||
|
||
await prisma.$disconnect();
|
||
}
|
||
|
||
main().catch((e) => {
|
||
console.error(e);
|
||
process.exit(1);
|
||
});
|