Pentest R86: Vertrags-Identifier max 100 + Charset-Whitelist
R86.1 LOW + R86.2 LOW: >999-Zeichen liefen in DB-Overflow (500
statt 400), Attribut-Injection (`foo" onerror=…` ohne
umschließenden Tag) überlebte stripHtml.
Fix: validateContractIdentifier() (max 100,
^[A-Za-z0-9_\-/. ]{0,100}$) in sanitize.ts, eingehängt in
sanitizeContractBody. Wirft ApiError(400, …). Literales Space
statt \s → kein CRLF/Tab → kein Header-Injection-Vektor in
CSV-/Mail-/PDF-Export. Greift auf alle fünf Identifier-Felder
(Provider + Sales-Platform). ContractForm-Inputs bekommen
maxLength={100} als UX-Schicht.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -504,6 +504,39 @@ erneut als „offenes Finding" auftaucht.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Runde 86 – Vertrags-Identifier-Validierung
|
||||
|
||||
**Findings (beide LOW):**
|
||||
|
||||
- **R86.1**: Strings >999 Zeichen in `orderNumberAtSalesPlatform` / den
|
||||
vier verwandten Sales-/Provider-Nummern-Feldern endeten mit
|
||||
generischem 500 (DB-Overflow `VARCHAR(191)`) statt sauberem 400.
|
||||
- **R86.2**: Attribut-Injection-Payload `foo" onerror="alert(1)`
|
||||
(kein umschließender Tag) überlebte `stripHtml`. React escaped
|
||||
Attribute, aber sobald der Wert in PDF-/Mail-/CSV-Export fließt,
|
||||
ist es potentiell aktiv.
|
||||
|
||||
**Fix:** `validateContractIdentifier(raw, fieldLabel)` in
|
||||
[`backend/src/utils/sanitize.ts`](../backend/src/utils/sanitize.ts):
|
||||
|
||||
- Max-Länge 100 Zeichen (deutlich unter VARCHAR(191)).
|
||||
- Whitelist `^[A-Za-z0-9_\-/. ]{0,100}$` – Buchstaben, Ziffern,
|
||||
Punkt, Bindestrich, Schrägstrich, Unterstrich und literales
|
||||
Leerzeichen. Bewusst NICHT `\s` (kein CRLF/Tab → kein
|
||||
Header-Injection-Vektor in CSV-/Mail-Exporten).
|
||||
- Bei Verstoß: `ApiError(400, …)` mit konkreter Fehlermeldung
|
||||
statt 500.
|
||||
- Eingehängt in `sanitizeContractBody` → läuft automatisch für alle
|
||||
fünf Identifier-Felder (`customerNumberAtProvider`,
|
||||
`contractNumberAtProvider`, `orderNumberAtSalesPlatform`,
|
||||
`customerNumberAtSalesPlatform`, `contractNumberAtSalesPlatform`)
|
||||
bei jedem Create/Update.
|
||||
- Frontend: `maxLength={100}` als zusätzliche UX-Schicht im
|
||||
ContractForm – Server-seitige Validierung bleibt die einzige
|
||||
Wahrheit, das HTML-Attribut spart nur den unnötigen Round-Trip.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Wann ist „dicht" dicht?
|
||||
|
||||
100 % gibt es nicht. Erreicht ist:
|
||||
|
||||
@@ -97,6 +97,21 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
||||
|
||||
## ✅ Erledigt
|
||||
|
||||
- [x] **🔒 Pentest R86 – Vertrags-Identifier härten**
|
||||
- R86.1 (LOW): >999-Zeichen-Strings auf Kunden-/Vertrags-/
|
||||
Auftragsnummer warfen 500 (DB-Overflow `VARCHAR(191)`) statt 400.
|
||||
- R86.2 (LOW/INFO): Attribut-Injection ohne umschließenden Tag
|
||||
(`foo" onerror=…`) überlebte `stripHtml` – kein Risiko in der React-
|
||||
UI, aber relevant für PDF/Mail/CSV-Export.
|
||||
- Fix: zentraler `validateContractIdentifier()` in `sanitize.ts`
|
||||
mit Max-100 und Whitelist `^[A-Za-z0-9_\-/. ]{0,100}$`. Bewusst
|
||||
literales Space statt `\s`, damit kein CRLF/Tab passiert (Header-
|
||||
Injection). Wirft `ApiError(400, …)` mit klarer Meldung.
|
||||
- Eingehängt in `sanitizeContractBody` → läuft automatisch für alle
|
||||
fünf Identifier-Felder bei Create/Update. ContractForm bekommt
|
||||
`maxLength={100}` als UX-Schicht. Doku in
|
||||
`docs/SECURITY-HARDENING.md` § Runde 86.
|
||||
|
||||
- [x] **🆕 Vertrag: Auftragsnummer bei Vertriebsplattform**
|
||||
- Neues optionales Feld `Contract.orderNumberAtSalesPlatform`
|
||||
(`VARCHAR(191) NULL`), Migration
|
||||
|
||||
Reference in New Issue
Block a user