Pentest 81.1 (MEDIUM): Self-Forward erzeugte Mail-Loop am Provider
Bug: Die Stressfrei-Adresse selbst (max@stressfrei-wechseln.net) konnte als zusätzliches Weiterleitungsziel eingetragen werden, auch Plus-Varianten. Plesk leitet auf sich selbst um → Mail-Loop. Backend setAdditionalForwards: lädt zusätzlich meta.email, vergleicht canonicalEmailKey gegen canonicalEmailKey(meta.email). Bei Treffer hartes ApiError(400) mit klarer "zeigt auf die Adresse selbst – Mail-Loop"-Meldung statt silent dedup – der User soll merken, dass sein Eintrag bewusst abgelehnt wurde. Frontend AdditionalForwardsModal: zusätzliche proaktive Validierung im Sub-Modal mit identischem canonicalize-Helper. Neuer selfEmail- Prop, damit auch der Create-Modus (vor Persist) den Check fahren kann. Spart Roundtrip + sofort sprechende Meldung.
This commit is contained in:
@@ -292,15 +292,21 @@ export async function deleteEmail(id: number) {
|
||||
*
|
||||
* Pentest 71.4: DB-Update wird bei Provider-Sync-Fehler zurückgerollt,
|
||||
* damit Plesk und DB nicht auseinanderlaufen.
|
||||
*
|
||||
* Pentest 81.1: Self-Forward wird hart abgelehnt – würde sonst am
|
||||
* Provider einen Mail-Loop erzeugen (Stressfrei-Adresse leitet auf
|
||||
* sich selbst um → unendliche Weiterleitung).
|
||||
*/
|
||||
export async function setAdditionalForwards(
|
||||
id: number,
|
||||
emails: string[],
|
||||
): Promise<{ success: boolean; forwardTargets?: string[]; error?: string }> {
|
||||
// Kunden-Stamm-Mail holen für Dedup gegen das (immer mit-gesetzte) Default-Ziel.
|
||||
// Kunden-Stamm-Mail + eigene Email holen für Dedup gegen die fest
|
||||
// gesetzten Ziele bzw. die Stressfrei-Adresse selbst.
|
||||
const meta = await prisma.stressfreiEmail.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
email: true,
|
||||
additionalForwardingEmails: true,
|
||||
customer: { select: { email: true } },
|
||||
},
|
||||
@@ -312,6 +318,7 @@ export async function setAdditionalForwards(
|
||||
const customerEmailKey = meta.customer?.email
|
||||
? canonicalEmailKey(meta.customer.email)
|
||||
: null;
|
||||
const selfKey = canonicalEmailKey(meta.email);
|
||||
|
||||
// Input normalisieren + Duplikate raus.
|
||||
const seen = new Set<string>();
|
||||
@@ -320,6 +327,16 @@ export async function setAdditionalForwards(
|
||||
for (const raw of emails) {
|
||||
const ok = assertValidForwardingEmail(raw);
|
||||
const key = canonicalEmailKey(ok);
|
||||
// 81.1: Eintrag, der auf die Adresse selbst zeigt, würde einen
|
||||
// Mail-Loop am Provider erzeugen. Hart ablehnen mit klarer
|
||||
// Fehlermeldung, statt silent zu droppen – der User soll merken,
|
||||
// dass sein Eintrag bewusst nicht akzeptiert wurde.
|
||||
if (key === selfKey) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
`"${ok}" zeigt auf die Adresse selbst – Mail-Loop. Bitte eine andere Weiterleitungsadresse wählen.`,
|
||||
);
|
||||
}
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
cleaned.push(ok);
|
||||
|
||||
@@ -97,6 +97,23 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
||||
|
||||
## ✅ Erledigt
|
||||
|
||||
- [x] **🔒 Pentest 81.1 (MEDIUM): Self-Forward erzeugte Mail-Loop am Provider**
|
||||
- Bug: User konnte die Stressfrei-Adresse selbst (z.B.
|
||||
`max.mustermann@stressfrei-wechseln.net`) als zusätzliches
|
||||
Weiterleitungsziel eintragen – auch Plus-Varianten davon. Plesk
|
||||
leitet auf sich selbst um → Mail-Loop.
|
||||
- Backend (`setAdditionalForwards`): zieht jetzt zusätzlich
|
||||
`meta.email` aus der DB und vergleicht `canonicalEmailKey(eintrag)`
|
||||
gegen `canonicalEmailKey(meta.email)`. Bei Treffer hartes
|
||||
`ApiError(400)` mit klarer Self-Forward-Meldung statt silent dedup
|
||||
– der User soll merken, dass sein Eintrag bewusst abgelehnt wurde.
|
||||
- Frontend (`AdditionalForwardsModal`): zusätzlich proaktive
|
||||
Validierung im Sub-Modal mit identischem `canonicalize`-Helper
|
||||
(Plus-Tag strippen, lowercase). Neuer Prop `selfEmail`, damit
|
||||
auch der Create-Modus (vor dem Persistieren) den Check fahren
|
||||
kann. Spart einen Roundtrip + zeigt sofort eine sprechende
|
||||
Meldung „… zeigt auf die Adresse selbst – Mail-Loop".
|
||||
|
||||
- [x] **🔒 Pentest 77.3 (LOW): `requireIdParam` ließ Float-IDs durch**
|
||||
- `Number.isInteger(parseInt('4.5'))` ist `true`, weil `parseInt`
|
||||
den Nachkomma-Teil silent abschneidet. Damit traf `/.../4.5/...`
|
||||
|
||||
@@ -4241,6 +4241,7 @@ function StressfreiEmailModal({
|
||||
onClose={() => setShowForwardsModal(false)}
|
||||
email={email ?? undefined}
|
||||
customerEmail={customerEmail}
|
||||
selfEmail={localPart ? localPart + domainSuffix : undefined}
|
||||
value={additionalForwards}
|
||||
onChange={setAdditionalForwards}
|
||||
/>
|
||||
@@ -4258,6 +4259,7 @@ function AdditionalForwardsModal({
|
||||
onClose,
|
||||
email,
|
||||
customerEmail,
|
||||
selfEmail,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
@@ -4265,6 +4267,9 @@ function AdditionalForwardsModal({
|
||||
onClose: () => void;
|
||||
email?: StressfreiEmail;
|
||||
customerEmail?: string;
|
||||
/** Die Stressfrei-Adresse selbst (für Self-Forward-Check im Create-Modus,
|
||||
* wo es noch kein `email`-Prop gibt). Edit-Modus zieht's aus `email`. */
|
||||
selfEmail?: string;
|
||||
/** Aktuelle Liste – im Create-Modus controlled, im Edit-Modus initialer Wert. */
|
||||
value: string[];
|
||||
onChange: (next: string[]) => void;
|
||||
@@ -4304,6 +4309,17 @@ function AdditionalForwardsModal({
|
||||
}
|
||||
};
|
||||
|
||||
// Plus-Tag wegstrippen + lowercase, identisch zum Backend-canonicalEmailKey.
|
||||
// Dann landen `billing+x@y` und `billing@y` im selben Key.
|
||||
const canonicalize = (raw: string) => {
|
||||
const lower = raw.trim().toLowerCase();
|
||||
const at = lower.lastIndexOf('@');
|
||||
if (at < 1) return lower;
|
||||
const local = lower.slice(0, at);
|
||||
const plus = local.indexOf('+');
|
||||
return (plus === -1 ? local : local.slice(0, plus)) + '@' + lower.slice(at + 1);
|
||||
};
|
||||
|
||||
const handleAdd = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const candidate = newEmail.trim().toLowerCase();
|
||||
@@ -4312,11 +4328,17 @@ function AdditionalForwardsModal({
|
||||
setError('Bitte eine gültige E-Mail-Adresse eingeben.');
|
||||
return;
|
||||
}
|
||||
if (candidate === customerEmail?.toLowerCase()) {
|
||||
const candidateKey = canonicalize(candidate);
|
||||
if (customerEmail && candidateKey === canonicalize(customerEmail)) {
|
||||
setError('Die Stamm-E-Mail des Kunden ist bereits Weiterleitungsziel.');
|
||||
return;
|
||||
}
|
||||
if (value.some((f) => f.toLowerCase() === candidate)) {
|
||||
const ownAddress = email?.email ?? selfEmail;
|
||||
if (ownAddress && candidateKey === canonicalize(ownAddress)) {
|
||||
setError(`"${candidate}" zeigt auf die Adresse selbst – das würde einen Mail-Loop erzeugen.`);
|
||||
return;
|
||||
}
|
||||
if (value.some((f) => canonicalize(f) === candidateKey)) {
|
||||
setError('Diese Adresse ist schon in der Liste.');
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user