Pentest 71.1-71.4: Härtung der Zusatz-Weiterleitungen

71.1 MEDIUM: BLOCKED_TLDS-Set in assertValidForwardingEmail –
reservierte/private TLDs (local, internal, corp, lan, home,
private, invalid, test, localhost, example, intranet, localdomain,
arpa) werden abgelehnt. Schließt Plesk-DNS-Probing ins interne Netz.

71.2 LOW: canonicalEmailKey-Helper normalisiert Mail-Adressen für
den Dedup (Plus-Tag wegstrippen, lowercase). billing+x@y und
billing@y haben jetzt denselben Schlüssel – auch gegen Kunden-
Stamm-Mail und gegen config.defaultForwardEmail im sync-Pfad.

71.3 INFO: Neuer requireIdParam-Helper im Controller liefert 400
statt 500 bei nicht-numerischen Route-IDs. Alle acht parseInt-
Stellen umgestellt (auch über die gemeldete eine hinaus).

71.4 INFO: setAdditionalForwards rollt den DB-Stand zurück, wenn
syncForwardingForEmail mit dem Provider scheitert. Vorheriger Wert
wird vorm Update gemerkt und im Fehlerfall wieder eingespielt –
DB und Plesk laufen nicht mehr auseinander.

Smoke-Tests: 11 reservierte TLDs abgelehnt, 4 echte TLDs (de, com,
co.uk, museum) durchgewinkt, Plus-Tag-Strip mit Multi-Plus+Casing.
This commit is contained in:
2026-06-18 13:41:16 +02:00
parent 96a054aa1a
commit 246999be01
3 changed files with 141 additions and 15 deletions
@@ -5,9 +5,23 @@ import { ApiResponse, AuthRequest } from '../types/index.js';
import { canAccessCustomer, canAccessStressfreiEmail } from '../utils/accessControl.js';
import { ApiError } from '../utils/apiError.js';
// Pentest 71.3 (INFO): `parseInt(...)` ohne NaN-Check gab bei
// `/stressfrei-emails/abc/...` einen generischen 500 zurück. Nicht
// kritisch, aber irreführend und log-spammend.
function requireIdParam(req: AuthRequest, res: Response, paramName: string): number | null {
const raw = req.params[paramName];
const parsed = Number.parseInt(raw, 10);
if (!Number.isInteger(parsed) || parsed < 1) {
res.status(400).json({ success: false, error: `Ungültige ID: ${raw}` } as ApiResponse);
return null;
}
return parsed;
}
export async function getEmailsByCustomer(req: AuthRequest, res: Response): Promise<void> {
try {
const customerId = parseInt(req.params.customerId);
const customerId = requireIdParam(req, res, 'customerId');
if (customerId === null) return;
// requireCustomerAccess in der Route greift nicht ausreichend:
// Portal-User haben `customers:read` (für eigene Daten) und werden
// dort short-circuited, ohne Owner-Vergleich. Pentest 2026-05-24
@@ -27,7 +41,8 @@ export async function getEmailsByCustomer(req: AuthRequest, res: Response): Prom
export async function getEmail(req: AuthRequest, res: Response): Promise<void> {
try {
const emailId = parseInt(req.params.id);
const emailId = requireIdParam(req, res, 'id');
if (emailId === null) return;
if (!(await canAccessStressfreiEmail(req, res, emailId))) return;
const email = await stressfreiEmailService.getEmailById(emailId);
@@ -55,7 +70,8 @@ export async function getEmail(req: AuthRequest, res: Response): Promise<void> {
export async function createEmail(req: Request, res: Response): Promise<void> {
try {
const customerId = parseInt(req.params.customerId);
const customerId = requireIdParam(req, res, 'customerId');
if (customerId === null) return;
const email = await stressfreiEmailService.createEmail({
...req.body,
customerId,
@@ -77,7 +93,8 @@ export async function createEmail(req: Request, res: Response): Promise<void> {
export async function updateEmail(req: AuthRequest, res: Response): Promise<void> {
try {
const emailId = parseInt(req.params.id);
const emailId = requireIdParam(req, res, 'id');
if (emailId === null) return;
if (!(await canAccessStressfreiEmail(req, res, emailId))) return;
const email = await stressfreiEmailService.updateEmail(emailId, req.body);
await logChange({
@@ -96,7 +113,8 @@ export async function updateEmail(req: AuthRequest, res: Response): Promise<void
export async function deleteEmail(req: AuthRequest, res: Response): Promise<void> {
try {
const emailId = parseInt(req.params.id);
const emailId = requireIdParam(req, res, 'id');
if (emailId === null) return;
if (!(await canAccessStressfreiEmail(req, res, emailId))) return;
await stressfreiEmailService.deleteEmail(emailId);
await logChange({
@@ -115,7 +133,8 @@ export async function deleteEmail(req: AuthRequest, res: Response): Promise<void
export async function syncForwarding(req: AuthRequest, res: Response): Promise<void> {
try {
const emailId = parseInt(req.params.id);
const emailId = requireIdParam(req, res, 'id');
if (emailId === null) return;
if (!(await canAccessStressfreiEmail(req, res, emailId))) return;
const result = await stressfreiEmailService.syncForwardingForEmail(emailId);
@@ -159,7 +178,8 @@ export async function syncForwarding(req: AuthRequest, res: Response): Promise<v
*/
export async function updateAdditionalForwards(req: AuthRequest, res: Response): Promise<void> {
try {
const emailId = parseInt(req.params.id);
const emailId = requireIdParam(req, res, 'id');
if (emailId === null) return;
if (!(await canAccessStressfreiEmail(req, res, emailId))) return;
const body = req.body ?? {};
@@ -202,7 +222,8 @@ export async function updateAdditionalForwards(req: AuthRequest, res: Response):
export async function resetPassword(req: AuthRequest, res: Response): Promise<void> {
try {
const emailId = parseInt(req.params.id);
const emailId = requireIdParam(req, res, 'id');
if (emailId === null) return;
if (!(await canAccessStressfreiEmail(req, res, emailId))) return;
const result = await stressfreiEmailService.resetMailboxPassword(emailId);
if (!result.success) {