Security-Hardening Runde 12: Information-Disclosure + Input-Validation

Pentest Runde 7 (Anschlussrunde):

MEDIUM – Interne Felder in Portal-Responses:
- sanitizeCustomerStrict strippt zusätzlich portalTokenInvalidatedAt,
  portalLastLogin, portalPasswordMustChange, lastBirthdayGreetingYear,
  privacyPolicyPath, businessRegistrationPath, commercialRegisterPath.
- Neue sanitizeContract/Strict + sanitizeContracts/Strict: entfernt
  portalPasswordEncrypted immer (nur über /password-Endpoint mit Audit
  abrufbar), für Portal-User zusätzlich commission/notes/nextReviewDate.
- getContract + getContracts wählen je nach isCustomerPortal die
  passende Variante. Mitarbeiter sehen commission/notes weiterhin.

LOW – Integer-Truncation bei IDs:
parseInt('6abc') → 6 lief vorher durch. Neue Heuristik-Middleware
unter /api: jedes Pfad-Segment, das mit Ziffer beginnt aber nicht
aus reinen Ziffern besteht, wird mit 400 abgelehnt. Trifft alle
Sub-Router ohne dass jede Route einzeln angefasst werden muss.

INFO – Rate-Limit: Code-Stand limit=10 für Login, limit=5 für
Password-Reset (lokal verifiziert: 11. failed login = 429). Pentester
sah vermutlich noch älteren Build. Kein Code-Change.

Live-verifiziert:
- /customers/6abc → 400 "Ungültige ID im URL-Pfad"
- /customers/3 → 200, /contracts/1abc/history → 400, normale Pfade OK
- Portal-User /customers/3: keine portalLastLogin/portalPasswordMustChange/
  portalTokenInvalidatedAt/etc. mehr in Response
- Portal-User /contracts/15: keine commission/notes/portalPasswordEncrypted/
  nextReviewDate
- Admin /contracts/15: commission/notes/nextReviewDate sichtbar,
  portalPasswordEncrypted weg

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 08:51:52 +02:00
parent c744eebfa3
commit 28c91759df
4 changed files with 147 additions and 4 deletions
+13 -2
View File
@@ -6,6 +6,7 @@ import * as contractHistoryService from '../services/contractHistory.service.js'
import * as authorizationService from '../services/authorization.service.js';
import { ApiResponse, AuthRequest } from '../types/index.js';
import { logChange } from '../services/audit.service.js';
import { sanitizeContract, sanitizeContractStrict, sanitizeContracts, sanitizeContractsStrict } from '../utils/sanitize.js';
import { canAccessContract } from '../utils/accessControl.js';
import { maybeActivateOnDeliveryConfirmation } from '../services/contractStatusScheduler.service.js';
@@ -46,9 +47,15 @@ export async function getContracts(req: AuthRequest, res: Response): Promise<voi
page: page ? parseInt(page as string) : undefined,
limit: limit ? parseInt(limit as string) : undefined,
});
// Portal-User bekommen die Strict-Variante (ohne commission/notes/
// nextReviewDate/portalPasswordEncrypted), Mitarbeiter die normale.
const isPortal = !!req.user?.isCustomerPortal;
const data = isPortal
? sanitizeContractsStrict(result.contracts as any[])
: sanitizeContracts(result.contracts as any[]);
res.json({
success: true,
data: result.contracts,
data,
pagination: result.pagination,
} as ApiResponse);
} catch (error) {
@@ -89,7 +96,11 @@ export async function getContract(req: AuthRequest, res: Response): Promise<void
}
}
res.json({ success: true, data: contract } as ApiResponse);
const isPortal = !!req.user?.isCustomerPortal;
const data = isPortal
? sanitizeContractStrict(contract as any)
: sanitizeContract(contract as any);
res.json({ success: true, data } as ApiResponse);
} catch (error) {
res.status(500).json({
success: false,