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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user