From e8d996d07e1a4baf6184371a1a34ecead8994c62 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 27 Jul 2026 16:41:19 +0200 Subject: [PATCH] Mobilfunknetz: server-seitige Whitelist (Pentest-INFO) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mobileNetwork akzeptierte serverseitig jeden stripHtml-bereinigten String – das Frontend-Dropdown beschränkte nur clientseitig. Jetzt Whitelist TELEKOM|VODAFONE|TELEFONICA (normalizeMobileNetwork), angewandt in Create- UND Update-Pfad (der Update-Spread reichte den Wert vorher ungefiltert an Prisma durch). Unbekannte/leere Werte werden zu null normalisiert. Co-Authored-By: Claude Opus 4.7 --- backend/src/services/contract.service.ts | 18 +++++++++++++++++- docs/todo.md | 8 ++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/backend/src/services/contract.service.ts b/backend/src/services/contract.service.ts index 1ed62d8d..25d12edd 100644 --- a/backend/src/services/contract.service.ts +++ b/backend/src/services/contract.service.ts @@ -322,6 +322,17 @@ interface ContractCreateData { }; } +// Erlaubte Mobilfunknetze. Server-seitige Whitelist, damit über einen +// handgebauten Request kein beliebiger String in mobileNetwork landet +// (das Frontend-Dropdown beschränkt schon, der Server vertraut ihm aber +// nicht). Unbekannte/leere Werte werden zu null normalisiert. +const ALLOWED_MOBILE_NETWORKS = new Set(['TELEKOM', 'VODAFONE', 'TELEFONICA']); +function normalizeMobileNetwork(value: unknown): string | null { + if (typeof value !== 'string') return null; + const v = value.trim().toUpperCase(); + return ALLOWED_MOBILE_NETWORKS.has(v) ? v : null; +} + export async function createContract(data: ContractCreateData) { const { energyDetails, @@ -387,7 +398,7 @@ export async function createContract(data: ContractCreateData) { mobileDetails: { create: { requiresMultisim: mobileDetails.requiresMultisim, - mobileNetwork: mobileDetails.mobileNetwork ?? null, + mobileNetwork: normalizeMobileNetwork(mobileDetails.mobileNetwork), dataVolume: mobileDetails.dataVolume, includedMinutes: mobileDetails.includedMinutes, includedSMS: mobileDetails.includedSMS, @@ -623,6 +634,11 @@ export async function updateContract( if (mobileDetails) { const { simCards, ...mobileData } = mobileDetails; + // Whitelist auf das Netz anwenden, bevor mobileData per Spread an + // Prisma geht (Update-Pfad reicht sonst beliebige Strings durch). + if ('mobileNetwork' in mobileData) { + mobileData.mobileNetwork = normalizeMobileNetwork(mobileData.mobileNetwork); + } const existing = await prisma.mobileContractDetails.findUnique({ where: { contractId: id }, }); diff --git a/docs/todo.md b/docs/todo.md index f67e646b..9449c920 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,14 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🔒 Mobilfunknetz: server-seitige Whitelist (Pentest-INFO)** + - `mobileNetwork` akzeptierte serverseitig jeden (stripHtml-bereinigten) + String – das Frontend-Dropdown beschränkte nur clientseitig. Jetzt + Whitelist `TELEKOM|VODAFONE|TELEFONICA` in `contract.service.ts` + (`normalizeMobileNetwork`), angewandt in Create- UND Update-Pfad + (Update-Spread wurde vorher ungefiltert durchgereicht). Unbekannte/ + leere Werte → null. + - [x] **🔒 Rate-Limiting: IPv6-Bypass-Härtung (ipKeyGenerator)** - Die Rate-Limiter mit eigenem `keyGenerator` (`loginRateLimiter`, `staffPasswordReAuthLimiter`) keyten auf die **volle** `req.ip`.