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:
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { pdfTemplateApi, contractApi, getAccessToken } from '../../services/api';
|
||||
import { pdfTemplateApi, contractApi, authApi } from '../../services/api';
|
||||
import type { PdfTemplate, CrmField, Contract } from '../../types';
|
||||
import Card from '../../components/ui/Card';
|
||||
import Button from '../../components/ui/Button';
|
||||
@@ -266,6 +266,16 @@ function FieldMappingModal({ template, onClose }: { template: PdfTemplate; onClo
|
||||
}, {} as Record<string, CrmField[]>);
|
||||
|
||||
const [highlightedField, setHighlightedField] = useState<string | null>(null);
|
||||
// Kurzlebigen Download-Token (60s) für die iframe-URL holen, damit das
|
||||
// langlebige Access-JWT nicht in Browser-History/Referer landet.
|
||||
const [previewToken, setPreviewToken] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
authApi.getDownloadToken().then((t) => {
|
||||
if (!cancelled) setPreviewToken(t);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [template.id]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex">
|
||||
@@ -275,11 +285,13 @@ function FieldMappingModal({ template, onClose }: { template: PdfTemplate; onClo
|
||||
<span>PDF-Vorschau mit Feldnamen</span>
|
||||
<span className="text-gray-400 text-xs">[Feldname] zeigt wo das Feld in der PDF liegt</span>
|
||||
</div>
|
||||
<iframe
|
||||
src={`/api/pdf-templates/${template.id}/preview?token=${getAccessToken() || ''}`}
|
||||
className="flex-1 w-full bg-white"
|
||||
title="PDF Vorschau mit Feldnamen"
|
||||
/>
|
||||
{previewToken && (
|
||||
<iframe
|
||||
src={`/api/pdf-templates/${template.id}/preview?token=${previewToken}`}
|
||||
className="flex-1 w-full bg-white"
|
||||
title="PDF Vorschau mit Feldnamen"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Zuordnung rechts */}
|
||||
@@ -428,11 +440,12 @@ function TestPreviewModal({ template, onClose }: { template: PdfTemplate; onClos
|
||||
});
|
||||
|
||||
const contracts: Contract[] = contractsData?.data || [];
|
||||
const token = getAccessToken();
|
||||
|
||||
const handleGenerate = () => {
|
||||
const handleGenerate = async () => {
|
||||
if (!selectedContractId) return;
|
||||
const url = `${pdfTemplateApi.generateUrl(template.id, selectedContractId)}?token=${token || ''}`;
|
||||
// Kurzlebigen Download-Token (60s) statt langlebigem Access-JWT in der URL.
|
||||
const downloadToken = await authApi.getDownloadToken();
|
||||
const url = `${pdfTemplateApi.generateUrl(template.id, selectedContractId)}?token=${downloadToken ?? ''}`;
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user