fix: Portal-Passwörter im Vertrag wurden mutiliert
Folgefehler aus Pentest 31.1: die rekursive sanitizeContractBody()
lief auch über portalPassword. Passwörter mit HTML-Pattern
("Pass<TAG>word!" → "Password!") oder URI-Schema-Prefix
("data:secret" → "blocked:secret") wurden vom stripHtml-Strip
zerstört, bevor die Service-Schicht sie verschlüsseln konnte.
Fix: PASSTHROUGH_KEYS = {portalPassword, password}. Beim Walk
werden String-Werte unter diesen Keys NICHT gefiltert. Passwort
wird sowieso encrypt()-verschlüsselt in die DB geschrieben und
niemals als HTML ausgegeben – kein XSS-Risk.
Live-verifiziert:
- PUT portalPassword="MyP@ss<word>123!&data:foo"
→ GET /password decrypt liefert byte-identischen Wert
- PUT providerName="<script>...EvilProvider" → DB: "EvilProvider"
(XSS-Schutz weiter aktiv)
- PUT portalUsername="u<test>" → DB: "u" (Plain-Text-User wird
weiter gestrippt, ist kein Passwort)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,15 +18,28 @@ import { maybeActivateOnDeliveryConfirmation } from '../services/contractStatusS
|
||||
* und lieferten sie 1:1 an Portal-User zurück. Verträge enthalten KEINE
|
||||
* HTML-Felder (Richtige HTML-Texte liegen in AppSettings), deshalb ist
|
||||
* Strip safe.
|
||||
*
|
||||
* AUSNAHME: Passwort-/Secret-Felder. `stripHtml` filtert `<…>`-Sequenzen
|
||||
* und URI-Schemata wie `data:`, also würde ein PW wie `Pass<TAG>word!`
|
||||
* zu `Password!` mutilieren oder `data:secret` zu `blocked:secret`.
|
||||
* Das Passwort wird sowieso verschlüsselt persistiert (`encrypt()`),
|
||||
* niemals als HTML ausgegeben – also kein XSS-Risk, und die Mangling
|
||||
* ist ein Bug (2026-05-27, intern gemeldet: "Portal-Passwörter werden
|
||||
* nicht gespeichert").
|
||||
*/
|
||||
function sanitizeContractBody(body: unknown): unknown {
|
||||
const PASSTHROUGH_KEYS = new Set(['portalPassword', 'password']);
|
||||
|
||||
function sanitizeContractBody(body: unknown, parentKey?: string): unknown {
|
||||
if (body === null || body === undefined) return body;
|
||||
if (typeof body === 'string') return stripHtml(body);
|
||||
if (Array.isArray(body)) return body.map(sanitizeContractBody);
|
||||
if (typeof body === 'string') {
|
||||
if (parentKey && PASSTHROUGH_KEYS.has(parentKey)) return body;
|
||||
return stripHtml(body);
|
||||
}
|
||||
if (Array.isArray(body)) return body.map((v) => sanitizeContractBody(v, parentKey));
|
||||
if (typeof body === 'object') {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(body as Record<string, unknown>)) {
|
||||
out[k] = sanitizeContractBody(v);
|
||||
out[k] = sanitizeContractBody(v, k);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -120,6 +120,21 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
||||
- **Live-verifiziert**: 4867 Datensätze + 1 Datei in 13.2s
|
||||
wiederhergestellt, Log-Modal zeigt den vollständigen Verlauf.
|
||||
|
||||
- [x] **🐛 Bugfix: Portal-Passwörter in Verträgen wurden mutiliert**
|
||||
- Folgefehler aus Pentest 31.1 (Stored-XSS-Strip): die rekursive
|
||||
`sanitizeContractBody`-Funktion lief auch über `portalPassword`.
|
||||
Passwörter mit HTML-Pattern (`Pass<TAG>word!` → `Password!`) oder
|
||||
URI-Schema-Prefix (`data:secret` → `blocked:secret`) wurden
|
||||
irreparabel zerstört.
|
||||
- Fix: `PASSTHROUGH_KEYS = {'portalPassword', 'password'}` – beim
|
||||
Walk werden String-Werte unter diesen Keys NICHT durch
|
||||
`stripHtml` geschickt. PW wird sowieso `encrypt()`-verschlüsselt
|
||||
persistiert und niemals als HTML ausgegeben → kein XSS-Risk.
|
||||
- Live-verifiziert: PW `MyP@ss<word>123!&data:foo` → byte-genau im
|
||||
GET-Decrypt-Endpoint zurück. `providerName: <script>…` → weiter
|
||||
auf `EvilProvider` gestrippt. `portalUsername: u<test>` → weiter
|
||||
auf `u` gestrippt.
|
||||
|
||||
- [x] **🛡️ Pentest 2026-05-24 Pen-31-Befunde (2× MEDIUM)**
|
||||
- **31.1 Stored XSS in Vertragsfeldern**: `providerName`, `tariffName`,
|
||||
`priceFirst12Months`, `priceFrom13Months`, `priceAfter24Months`
|
||||
|
||||
Reference in New Issue
Block a user