Portal-Passwort: Reveal/Send prueft Konsistenz gegen Login-Hash
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>
This commit is contained in:
@@ -408,23 +408,50 @@ export async function markPortalPasswordForChange(customerId: number) {
|
||||
});
|
||||
}
|
||||
|
||||
// Kundenportal-Passwort im Klartext abrufen
|
||||
export async function getCustomerPortalPassword(customerId: number): Promise<string | null> {
|
||||
// Ergebnis des Klartext-Passwort-Abrufs.
|
||||
// - 'ok' → password ist das gültige Klartext-Passwort
|
||||
// - 'none' → kein verschlüsseltes Passwort hinterlegt (z.B. self-service
|
||||
// gesetzt) → nichts zum Anzeigen/Versenden
|
||||
// - 'desync' → verschlüsseltes Passwort vorhanden, passt aber NICHT zum
|
||||
// Login-Hash (oder Entschlüsselung fehlgeschlagen). Darf NIE
|
||||
// ausgegeben werden – sonst liest der Support ein Passwort vor,
|
||||
// das beim Login scheitert (Pentest-Hinweis).
|
||||
export type PortalPasswordReveal =
|
||||
| { status: 'ok'; password: string }
|
||||
| { status: 'none' }
|
||||
| { status: 'desync' };
|
||||
|
||||
// Kundenportal-Passwort im Klartext abrufen – mit Konsistenzprüfung gegen den
|
||||
// Login-Hash, damit niemals ein „falsches" (out-of-sync) Passwort herausgeht.
|
||||
export async function getCustomerPortalPassword(customerId: number): Promise<PortalPasswordReveal> {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: { portalPasswordEncrypted: true },
|
||||
select: { portalPasswordEncrypted: true, portalPasswordHash: true },
|
||||
});
|
||||
|
||||
if (!customer?.portalPasswordEncrypted) {
|
||||
return null;
|
||||
return { status: 'none' };
|
||||
}
|
||||
|
||||
let decrypted: string;
|
||||
try {
|
||||
return decrypt(customer.portalPasswordEncrypted);
|
||||
decrypted = decrypt(customer.portalPasswordEncrypted);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Entschlüsseln des Passworts:', error);
|
||||
return null;
|
||||
return { status: 'desync' };
|
||||
}
|
||||
|
||||
// Muss zum Login-Hash passen – sonst ist der gespeicherte Klartext veraltet
|
||||
// und würde beim Login fehlschlagen.
|
||||
if (customer.portalPasswordHash) {
|
||||
const matches = await bcrypt.compare(decrypted, customer.portalPasswordHash);
|
||||
if (!matches) {
|
||||
console.warn(`[getCustomerPortalPassword] Desync: gespeichertes Passwort passt nicht zum Login-Hash (Kunde #${customerId})`);
|
||||
return { status: 'desync' };
|
||||
}
|
||||
}
|
||||
|
||||
return { status: 'ok', password: decrypted };
|
||||
}
|
||||
|
||||
export async function createUser(data: {
|
||||
|
||||
Reference in New Issue
Block a user