Pentest R95: portalUsername (Manual-Modus) härten
R95.1 MEDIUM: foo\r\nBcc:evil@x.de → Header-Injection-Vektor R95.3 LOW: <script>...</script>@x.de → silent stripHtml-Mutation R95.4 LOW: >190 Zeichen → VARCHAR-Overflow → 500 statt 400 Fix: validatePortalUsername() in sanitize.ts mit Whitelist ^[A-Za-z0-9_\-/.@+ ]{0,100}$. Strukturell sind CRLF, Tab, alle Control-Chars, Tags und Quotes raus → R95.1+R95.3 ohne extra Check. Max 100 → ApiError(400) → R95.4. Raw-Input vor stripHtml geprüft (R87-Pattern). Eingehängt in sanitizeContractBody. R95.2 (Email-Format-Pflicht) bewusst NICHT übernommen: portalUsername ist im Manual-Modus nicht zwingend eine Email (Vodafone, 1&1, EWE und Stadtwerke nutzen Kundennummern oder Pseudonyme als Portal-Login). Doku in SECURITY-HARDENING.md § Runde 95. Frontend: maxLength={100} am Input als UX-Schicht. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -614,6 +614,46 @@ einer mehrzeiligen Postadresse.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Runde 95 – Portal-Username-Validierung
|
||||
|
||||
**Findings (R95.1 MEDIUM + R95.3 LOW + R95.4 LOW):**
|
||||
|
||||
`portalUsername` (Manual-Input-Modus am Vertrag) hatte gar keine
|
||||
Validierung. Drei nachweisbare Effekte:
|
||||
|
||||
- **R95.1**: `foo\r\nBcc:evil@x.de` (CRLF) wurde verbatim
|
||||
gespeichert → Header-Injection-Vektor sobald der Wert in
|
||||
Mail-Templates oder PDF-Footers landet.
|
||||
- **R95.3**: `<script>alert(1)</script>@x.de` lief durch
|
||||
`stripHtml` → stille Mutation zu `@x.de` (R87.1/R89.2-Pattern
|
||||
auf neuem Feld).
|
||||
- **R95.4**: >190 Zeichen → VARCHAR-Overflow → generischer 500
|
||||
statt sauberem 400.
|
||||
|
||||
**Fix:** `validatePortalUsername(raw, fieldLabel)` in
|
||||
[`backend/src/utils/sanitize.ts`](../backend/src/utils/sanitize.ts):
|
||||
|
||||
- Whitelist `^[A-Za-z0-9_\-/.@+ ]{0,100}$`. Strukturell sind
|
||||
CRLF, Tab, alle Control-Chars, Tags (`<`, `>`) und Quotes raus
|
||||
→ R95.1 und R95.3 ohne extra Check.
|
||||
- Max 100 Zeichen → `ApiError(400, …)` → R95.4 mit klarer Meldung.
|
||||
- Raw-Input direkt validiert (kein `stripHtml` davor) – gleicher
|
||||
R87-Pattern wie bei Contract-Identifier und Provider-Address.
|
||||
- Eingehängt in `sanitizeContractBody` als eigener Branch.
|
||||
|
||||
**Bewusst NICHT übernommen: R95.2 (Email-Format-Pflicht).**
|
||||
|
||||
Der Pentester schlägt `z.string().email()` vor, weil der „Kunde
|
||||
sich sonst nicht einloggen kann". Falsche Annahme: `portalUsername`
|
||||
ist im Manual-Modus **nicht zwingend eine E-Mail**. Vodafone, 1&1,
|
||||
EWE und etliche Stadtwerke nutzen reine Kundennummern (`12345678`),
|
||||
Pseudonyme (`max.mustermann`) oder Customer-IDs als Portal-Login.
|
||||
Eine Email-Pflicht würde legitime Logins ablehnen. Der Stressfrei-
|
||||
Modus hängt sowieso an einer schon validierten Email-Stammdate
|
||||
(`assertValidForwardingEmail`).
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Wann ist „dicht" dicht?
|
||||
|
||||
100 % gibt es nicht. Erreicht ist:
|
||||
|
||||
Reference in New Issue
Block a user