Security-Hardening Runde 11: Pentest Runde 7 (Portal-PW + Download-Tokens)
Hit-List vom Pentester abgearbeitet. Hauptpunkte:
1) Contract/Mail-Credentials (password/internet/sip/simcard, mailbox/send/
reset-password): ALLE bereits durch canAccess* gesichert, keine Lücke.
2) GET /customers/:id/portal/password (Klartext-Portal-PW-Abruf):
fehlender canAccessCustomer-Check ergänzt. Defense in depth gegen
versehentliche customers:update-Permission an Portal/eingeschränkte
Mitarbeiter.
3) Admin-Endpoints (factory-reset, developer/*, audit-logs/rehash,
audit-logs/customer): durch admin-Permissions geschützt – Portal-User
haben diese nicht.
4) Token-in-URL (NIEDRIG): Langlebige Access-JWTs landeten als ?token= in
URLs für iframe-PDFs, Audit-Export-Window etc. → nginx-Logs +
Browser-History + Referer.
Lösung: kurzlebige Download-Tokens.
- signDownloadToken() liefert JWT mit type='download', exp=60s
- Auth-Middleware akzeptiert type='download' AUSSCHLIESSLICH via
?token=, niemals als Bearer-Header
- POST /api/auth/download-token Endpoint (authenticated)
- Frontend: authApi.getDownloadToken() utility
- 4 Stellen migriert: AuditLog-Export, PdfTemplate-Preview-iframe,
PdfTemplate-Generate, ContractDetail-PDF-Generate (2x),
Portal-Privacy-PDF
- fileUrl/getAttachmentUrl sind synchron + breit gestreut – Migration
bleibt für Folge-PR
Live-verifiziert:
- Download-Token: 1773 Zeichen, type=download, exp-iat=60s
- als Header → 401 (Falscher Token-Typ), als ?token= → 200
- portal-user (Customer 3) auf customers/2/portal/password → 403
Rate-Limiter-Check: express-rate-limit Fixed-Window, kein Reset bei jedem
Request (Pentester-Klage „Fenster reseted sich" stimmt mit dem Code nicht
überein – wahrscheinlich Retry-After-Misinterpretation). Kein Code-Bug
identifiziert; ggf. später Admin-Override-Endpoint nachrüsten.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -406,6 +406,36 @@ export async function register(req: Request, res: Response): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Kurzlebiger Download-Token (60s) für Aufrufe, die den Token in der URL
|
||||
// brauchen (PDF-iframes, window.open für Audit-Export usw.). Aufrufer
|
||||
// authentifiziert sich normal per Bearer-Header. Antwort: ein download-
|
||||
// scoped JWT, das die Auth-Middleware nur via `?token=` akzeptiert.
|
||||
export async function createDownloadToken(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ success: false, error: 'Nicht authentifiziert' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
const payload: any = {
|
||||
email: req.user.email,
|
||||
permissions: req.user.permissions,
|
||||
isCustomerPortal: !!req.user.isCustomerPortal,
|
||||
};
|
||||
if (req.user.userId) payload.userId = req.user.userId;
|
||||
if (req.user.customerId) payload.customerId = req.user.customerId;
|
||||
if ((req.user as any).representedCustomerIds) {
|
||||
payload.representedCustomerIds = (req.user as any).representedCustomerIds;
|
||||
}
|
||||
const token = authService.signDownloadToken(payload);
|
||||
res.json({ success: true, data: { token } } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Erstellen des Download-Tokens',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Vom Endkunden selbst nach Einmalpasswort-Login aufgerufen, um sein eigenes
|
||||
// Passwort zu vergeben. Server invalidiert die laufende Session, Frontend
|
||||
// loggt aus und schickt zurück zum Login.
|
||||
|
||||
@@ -1096,9 +1096,10 @@ export async function setPortalPassword(req: Request, res: Response): Promise<vo
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPortalPassword(req: Request, res: Response): Promise<void> {
|
||||
export async function getPortalPassword(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
if (!(await canAccessCustomer(req, res, customerId))) return;
|
||||
const password = await authService.getCustomerPortalPassword(customerId);
|
||||
// Klartext-Passwort-Read auditieren (CRITICAL): wer hat wann das Portal-
|
||||
// Passwort eines Kunden entschlüsselt? Wichtig für DSGVO-Nachvollziehbarkeit
|
||||
|
||||
Reference in New Issue
Block a user