Kunde: E-Mail Pflichtfeld + keine verwaltete Provider-Domain erlaubt
Die private Kunden-E-Mail (Customer.email) darf nicht auf einer der bei den E-Mail-Providern konfigurierten Domains (bzw. Subdomains) liegen - sonst traegt man versehentlich eine unserer verwalteten Weiterleitungs-/Mailbox- Adressen als private Adresse ein. Zudem ist E-Mail jetzt Pflichtfeld. Backend: createCustomer/updateCustomer pruefen Pflicht + Domain (neue Helper getConfiguredEmailDomains/emailUsesDomain im emailProvider-Service); email aus nullableFields entfernt. Frontend: E-Mail-Feld required + Hinweis. Verifiziert: Provider-Domain + Subdomain (case-insensitiv) verboten, Fremd-Domains erlaubt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import prisma from '../lib/prisma.js';
|
||||
import * as customerService from '../services/customer.service.js';
|
||||
import * as authService from '../services/auth.service.js';
|
||||
import { logChange } from '../services/audit.service.js';
|
||||
import { getConfiguredEmailDomains, emailUsesDomain } from '../services/emailProvider/emailProviderService.js';
|
||||
import { validatePasswordComplexity, generateSecurePassword } from '../utils/passwordGenerator.js';
|
||||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||||
import {
|
||||
@@ -99,12 +100,23 @@ export async function createCustomer(req: Request, res: Response): Promise<void>
|
||||
try {
|
||||
// Whitelist: nur erlaubte Felder aus req.body übernehmen
|
||||
const data: any = pickCustomerCreate(req.body);
|
||||
// E-Mail ist Pflichtfeld.
|
||||
if (!data.email || !String(data.email).trim()) {
|
||||
res.status(400).json({ success: false, error: 'E-Mail-Adresse ist ein Pflichtfeld.' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
// Email-Format prüfen, sonst landet "test@x.de\nBcc:evil@..." als
|
||||
// SMTP-Header-Injection-Vektor in der DB (Pentest 29.4).
|
||||
if (data.email && !isValidEmail(data.email)) {
|
||||
res.status(400).json({ success: false, error: 'Ungültiges E-Mail-Format' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
// Private E-Mail darf keine unserer verwalteten Provider-/Weiterleitungs-
|
||||
// Domains verwenden.
|
||||
if (emailUsesDomain(String(data.email), await getConfiguredEmailDomains())) {
|
||||
res.status(400).json({ success: false, error: 'Diese E-Mail liegt auf einer verwalteten (Weiterleitungs-/Mailbox-)Domain und ist als private Kunden-E-Mail nicht zulässig.' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
if (data.portalEmail && !isValidEmail(data.portalEmail)) {
|
||||
res.status(400).json({ success: false, error: 'Ungültiges Portal-E-Mail-Format' } as ApiResponse);
|
||||
return;
|
||||
@@ -159,11 +171,21 @@ export async function updateCustomer(req: Request, res: Response): Promise<void>
|
||||
res.status(400).json({ success: false, error: 'Ungültiges E-Mail-Format' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
// Private E-Mail darf keine verwaltete Provider-/Weiterleitungs-Domain sein.
|
||||
if (req.body?.email && emailUsesDomain(String(req.body.email), await getConfiguredEmailDomains())) {
|
||||
res.status(400).json({ success: false, error: 'Diese E-Mail liegt auf einer verwalteten (Weiterleitungs-/Mailbox-)Domain und ist als private Kunden-E-Mail nicht zulässig.' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
if (req.body?.portalEmail && !isValidEmail(req.body.portalEmail)) {
|
||||
res.status(400).json({ success: false, error: 'Ungültiges Portal-E-Mail-Format' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
const data: any = pickCustomerUpdate(req.body);
|
||||
// E-Mail ist Pflichtfeld – darf per Update nicht geleert werden.
|
||||
if ('email' in data && (!data.email || !String(data.email).trim())) {
|
||||
res.status(400).json({ success: false, error: 'E-Mail-Adresse ist ein Pflichtfeld.' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pentest 60.3 (MEDIUM, 2026-06-01): pickCustomerUpdate macht nur
|
||||
// stripHtml; CRLF und andere Control-Chars überlebten. Phone/Mobile
|
||||
@@ -192,7 +214,8 @@ export async function updateCustomer(req: Request, res: Response): Promise<void>
|
||||
data.birthDate = new Date(data.birthDate);
|
||||
}
|
||||
// Leere Strings in optionalen Feldern zu null konvertieren
|
||||
const nullableFields = ['salutation', 'birthPlace', 'phone', 'mobile', 'email', 'companyName', 'taxNumber', 'businessRegistration', 'commercialRegister', 'commercialRegisterNumber', 'notes'];
|
||||
// 'email' NICHT nullable – ist Pflichtfeld (leer wird oben schon abgelehnt).
|
||||
const nullableFields = ['salutation', 'birthPlace', 'phone', 'mobile', 'companyName', 'taxNumber', 'businessRegistration', 'commercialRegister', 'commercialRegisterNumber', 'notes'];
|
||||
for (const field of nullableFields) {
|
||||
if (data[field] === '') data[field] = null;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,32 @@ export async function getActiveProviderConfig() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle konfigurierten Provider-Domains (lowercase, dedupliziert). Das sind
|
||||
* UNSERE verwalteten Mailbox-/Weiterleitungs-Domains (z.B. stressfrei-wechseln.de)
|
||||
* – eine private Kunden-E-Mail darf darauf NICHT liegen, sonst trägt man
|
||||
* versehentlich eine unserer Weiterleitungsadressen als private Adresse ein.
|
||||
*/
|
||||
export async function getConfiguredEmailDomains(): Promise<string[]> {
|
||||
const configs = await prisma.emailProviderConfig.findMany({ select: { domain: true } });
|
||||
const domains = configs
|
||||
.map((c) => (c.domain || '').trim().toLowerCase())
|
||||
.filter((d) => d.length > 0);
|
||||
return [...new Set(domains)];
|
||||
}
|
||||
|
||||
/**
|
||||
* true, wenn die E-Mail auf einer der übergebenen Domains (oder einer Subdomain
|
||||
* davon) liegt. Vergleich case-insensitiv.
|
||||
*/
|
||||
export function emailUsesDomain(email: string, domains: string[]): boolean {
|
||||
const at = email.lastIndexOf('@');
|
||||
if (at < 0) return false;
|
||||
const host = email.slice(at + 1).trim().toLowerCase();
|
||||
if (!host) return false;
|
||||
return domains.some((d) => host === d || host.endsWith('.' + d));
|
||||
}
|
||||
|
||||
export interface CreateProviderConfigData {
|
||||
name: string;
|
||||
type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN';
|
||||
|
||||
@@ -198,11 +198,17 @@ export default function CustomerForm() {
|
||||
|
||||
<Card className="mb-6" title="Kontaktdaten">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Input
|
||||
label="E-Mail"
|
||||
label="E-Mail *"
|
||||
type="email"
|
||||
{...register('email')}
|
||||
{...register('email', { required: 'E-Mail ist erforderlich' })}
|
||||
error={errors.email?.message}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Private E-Mail des Kunden – keine verwaltete Weiterleitungs-/Mailbox-Domain.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label="Telefon"
|
||||
|
||||
Reference in New Issue
Block a user