From 18a2e1173b9a8aa5fd3b8599e4a482d7bb51ef75 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 21 Jun 2026 14:22:40 +0200 Subject: [PATCH] Pentest R91: NaN-Bypass auf accountId-Query-Param MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R91.1 LOW: parseInt('abc') = NaN → der Ternary gab NaN an den Service, if (NaN) ist falsy → Postfach-Filter fiel weg. Portal- User mit ungültigem accountId sah Mails aus allen Postfächern des Kunden für seinen Vertrag (canAccessContract greift weiter, kein Cross-Customer-Leak). Fix: zentraler parsePositiveIntParam(), akzeptiert nur positive Ganzzahlen aus Query-Strings. Eingesetzt auf allen 5 Endpunkten, die accountId/contractId aus Query lesen – auch da, wo der Pentester nicht getestet hat (Customer-Inbox, Trash-Count), weil derselbe Pattern überall stand. Co-Authored-By: Claude Opus 4.7 --- .../src/controllers/cachedEmail.controller.ts | 33 +++++++++++++++---- docs/todo.md | 15 +++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/backend/src/controllers/cachedEmail.controller.ts b/backend/src/controllers/cachedEmail.controller.ts index f1de989c..2e82b593 100644 --- a/backend/src/controllers/cachedEmail.controller.ts +++ b/backend/src/controllers/cachedEmail.controller.ts @@ -41,12 +41,31 @@ function parseDateParam(v: unknown): Date | undefined { return isNaN(d.getTime()) ? undefined : d; } +// Pentest 91.1 (LOW, 2026-06-21): `accountId=abc` (→ parseInt = NaN) und +// `accountId=` (leer string ist truthy nach `req.query.accountId ? …`) +// kamen vorher als `NaN` im Service an. `if (NaN)` ist falsy → der +// stressfreiEmailId-Filter wurde komplett übersprungen, und der Vertrag +// zeigte Mails aus ALLEN Postfächern. +// +// Helper akzeptiert nur positive Ganzzahlen; alles andere → undefined, +// das im Service den Filter wegfallen lässt. Wenn wir wollten dass +// invalide Werte 400 werfen statt silent ignoriert zu werden, müssten +// wir das pro Endpunkt entscheiden – die Semantik „Filter weglassen" +// ist hier in Ordnung, weil der `customerId`-/`contractId`-Constraint +// im `where` weiterhin greift (kein Cross-Customer-Leak). +function parsePositiveIntParam(v: unknown): number | undefined { + if (typeof v !== 'string' || v.trim() === '') return undefined; + const n = parseInt(v, 10); + if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) return undefined; + return n; +} + // E-Mails für einen Kunden abrufen export async function getEmailsForCustomer(req: AuthRequest, res: Response): Promise { try { const customerId = parseInt(req.params.customerId); if (!(await canAccessCustomer(req, res, customerId))) return; - const stressfreiEmailId = req.query.accountId ? parseInt(req.query.accountId as string) : undefined; + const stressfreiEmailId = parsePositiveIntParam(req.query.accountId); const folder = req.query.folder as string | undefined; // INBOX oder SENT const limit = req.query.limit ? parseInt(req.query.limit as string) : 50; const offset = req.query.offset ? parseInt(req.query.offset as string) : 0; @@ -90,7 +109,7 @@ export async function getEmailsForContract(req: AuthRequest, res: Response): Pro const contractId = parseInt(req.params.contractId); if (!(await canAccessContract(req, res, contractId))) return; const folder = req.query.folder as string | undefined; // INBOX oder SENT - const stressfreiEmailId = req.query.accountId ? parseInt(req.query.accountId as string) : undefined; + const stressfreiEmailId = parsePositiveIntParam(req.query.accountId); const limit = req.query.limit ? parseInt(req.query.limit as string) : 50; const offset = req.query.offset ? parseInt(req.query.offset as string) : 0; @@ -248,7 +267,7 @@ export async function getContractFolderCounts(req: AuthRequest, res: Response): try { const contractId = parseInt(req.params.contractId); if (!(await canAccessContract(req, res, contractId))) return; - const stressfreiEmailId = req.query.accountId ? parseInt(req.query.accountId as string) : undefined; + const stressfreiEmailId = parsePositiveIntParam(req.query.accountId); const counts = await cachedEmailService.getFolderCountsForContract(contractId, stressfreiEmailId); @@ -896,8 +915,8 @@ export async function getTrashEmails(req: AuthRequest, res: Response): Promise