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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user