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:
@@ -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 } });
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import prisma from '../lib/prisma.js';
|
||||
import * as userService from '../services/user.service.js';
|
||||
import { logChange } from '../services/audit.service.js';
|
||||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||||
import { pickUserCreate, pickUserUpdate, isValidEmail } from '../utils/sanitize.js';
|
||||
import { pickUserCreate, pickUserUpdate, isValidEmail, sanitizePhoneField } from '../utils/sanitize.js';
|
||||
import { validatePasswordComplexity, STAFF_MIN_PASSWORD_LENGTH } from '../utils/passwordGenerator.js';
|
||||
|
||||
// Users
|
||||
@@ -70,6 +70,20 @@ export async function createUser(req: Request, res: Response): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 60.3: WhatsApp- und Signal-Nummern gegen CRLF/Header-Injection sichern.
|
||||
try {
|
||||
if ('whatsappNumber' in data) {
|
||||
const cleaned = sanitizePhoneField(data.whatsappNumber, 'WhatsApp-Nummer');
|
||||
data.whatsappNumber = cleaned ?? null;
|
||||
}
|
||||
if ('signalNumber' in data) {
|
||||
const cleaned = sanitizePhoneField(data.signalNumber, 'Signal-Nummer');
|
||||
data.signalNumber = cleaned ?? null;
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültige Nummer' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
const user = await userService.createUser(data);
|
||||
await logChange({
|
||||
req, action: 'CREATE', resourceType: 'User',
|
||||
@@ -114,13 +128,27 @@ export async function updateUser(req: Request, res: Response): Promise<void> {
|
||||
return;
|
||||
}
|
||||
// Whitelist: nur erlaubte Felder aus req.body übernehmen (Mass-Assignment-Schutz)
|
||||
const data = pickUserUpdate(req.body);
|
||||
const data = pickUserUpdate(req.body) as Record<string, unknown>;
|
||||
// Email-Validierung gegen SMTP-Header-Injection (Pentest 29.4).
|
||||
// null/leer ist OK (Email darf optional sein), nur falsches Format prüfen.
|
||||
if (data?.email !== undefined && !isValidEmail(data.email)) {
|
||||
res.status(400).json({ success: false, error: 'Ungültiges E-Mail-Format' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
// 60.3: WhatsApp- und Signal-Nummern gegen CRLF/Header-Injection sichern.
|
||||
try {
|
||||
if ('whatsappNumber' in data) {
|
||||
const cleaned = sanitizePhoneField(data.whatsappNumber, 'WhatsApp-Nummer');
|
||||
data.whatsappNumber = cleaned ?? null;
|
||||
}
|
||||
if ('signalNumber' in data) {
|
||||
const cleaned = sanitizePhoneField(data.signalNumber, 'Signal-Nummer');
|
||||
data.signalNumber = cleaned ?? null;
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Ungültige Nummer' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vorherigen Stand laden für Audit – inkl. Rollen, damit hasGdprAccess /
|
||||
// hasDeveloperAccess (versteckte Rollen) korrekt verglichen werden.
|
||||
|
||||
Reference in New Issue
Block a user