Pentest 60.3 MEDIUM: sanitizePhoneField auf Customer + User-Felder ausweiten

Der Fix aus 51.3 deckte nur Contract-PhoneNumber-Felder ab. CRLF in
`Customer.phone`, `Customer.mobile` und (im selben Code-Pfad)
`User.whatsappNumber`, `User.signalNumber` ging weiter durch –
pickCustomerUpdate / pickUserUpdate macht nur stripHtml, das filtert
keine Control-Chars.

- sanitizePhoneField von contract.service nach utils/sanitize gezogen
  und EXPORT, damit alle Stellen denselben Allowlist-Check
  (/^[0-9+\-/(). ]{0,40}$/) nutzen. Literales Space, NICHT \s.
- customer.controller updateCustomer + createCustomer: phone + mobile
  durch sanitizePhoneField → 400 bei CRLF/Control-Chars.
- user.controller updateUser + createUser: whatsappNumber +
  signalNumber analog.
- contract.service nutzt jetzt den importierten Helper (Lokale
  Kopie entfernt – Single Source of Truth).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 22:40:40 +02:00
parent f4ac1c29db
commit 5fa9d4d4f3
4 changed files with 84 additions and 21 deletions
@@ -11,6 +11,7 @@ import {
sanitizeCustomerStrict,
pickCustomerCreate,
pickCustomerUpdate,
sanitizePhoneField,
isValidEmail,
} from '../utils/sanitize.js';
import {
@@ -90,6 +91,20 @@ export async function createCustomer(req: Request, res: Response): Promise<void>
res.status(400).json({ success: false, error: 'Ungültiges Portal-E-Mail-Format' } as ApiResponse);
return;
}
// 60.3: Phone/Mobile auch beim Create gegen Header-Injection sichern.
try {
if ('phone' in data) {
const cleaned = sanitizePhoneField(data.phone, 'Telefon');
data.phone = cleaned ?? null;
}
if ('mobile' in data) {
const cleaned = sanitizePhoneField(data.mobile, 'Mobil');
data.mobile = cleaned ?? null;
}
} catch (err) {
res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültige Telefonnummer' } as ApiResponse);
return;
}
// Convert birthDate string to Date if present
if (data.birthDate) {
data.birthDate = new Date(data.birthDate);
@@ -132,6 +147,23 @@ export async function updateCustomer(req: Request, res: Response): Promise<void>
}
const data: any = pickCustomerUpdate(req.body);
// Pentest 60.3 (MEDIUM, 2026-06-01): pickCustomerUpdate macht nur
// stripHtml; CRLF und andere Control-Chars überlebten. Phone/Mobile
// jetzt zusätzlich durch sanitizePhoneField (Allowlist).
try {
if ('phone' in data) {
const cleaned = sanitizePhoneField(data.phone, 'Telefon');
data.phone = cleaned ?? null;
}
if ('mobile' in data) {
const cleaned = sanitizePhoneField(data.mobile, 'Mobil');
data.mobile = cleaned ?? null;
}
} catch (err) {
res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültige Telefonnummer' } as ApiResponse);
return;
}
// Vorherigen Stand laden für Audit
const before = await prisma.customer.findUnique({ where: { id: customerId } });