Pentest 47.1/47.2/47.3: Re-Auth bei sensiblen Operationen + Provider.name-Strip
47.3 MEDIUM (Admin-Passwort-Reset ohne Re-Auth): POST /api/users/:id/password verlangt jetzt currentPassword im Body. Backend prüft per bcrypt.compare gegen den Hash des aufrufenden Admins. Frontend (UserList-Modal): zusätzliches Passwort-Feld wird eingeblendet, sobald für einen User ein neues Passwort gesetzt werden soll. Gestohlener JWT allein reicht damit nicht mehr. 47.1 MEDIUM (Open Redirect / Phishing via provider.portalUrl): Selbes Re-Auth-Pattern für Provider-Endpoints. Nur wenn die Portal-URL-Domain WIRKLICH gewechselt wird (Host-Vergleich) oder beim Create mit URL, ist currentPassword Pflicht. Reine Namens-/Tarif-Edits bleiben friction-frei. Audit-Log bekommt die Portal-URL beim Ändern explizit mitgeloggt (Forensik bei Vorfällen). Frontend ProviderModal zeigt amber- farbenen Bestätigungs-Banner mit Passwort-Eingabe sobald der Host wechselt. 47.2 INFO (provider.name ohne Backend-Sanitization): Neuer Helper stripProviderStrings in provider.service, wendet stripHtml auf name + usernameFieldName + passwordFieldName an – Defense-in-Depth gegen neue Renderpfade (PDF, Mail-Templates). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import prisma from '../lib/prisma.js';
|
||||
import { stripHtml } from '../utils/sanitize.js';
|
||||
import { validateHttpUrl } from '../utils/url.js';
|
||||
|
||||
// Pentest 46.1 (HIGH, 2026-06-01): Stored XSS via provider.portalUrl.
|
||||
@@ -46,16 +47,29 @@ export async function getProviderById(id: number) {
|
||||
});
|
||||
}
|
||||
|
||||
// Pentest 47.2 (INFO, 2026-06-01): provider.name landete roh in der DB.
|
||||
// Aktuell escapt React das Textnode, also kein direkter XSS – aber neue
|
||||
// Renderpfade (PDF, Mail-Templates, Embedded-Strings in URLs) wären
|
||||
// sofort betroffen. Defense-in-depth: schon beim Schreiben strippen.
|
||||
function stripProviderStrings<T extends { name?: string; usernameFieldName?: string; passwordFieldName?: string }>(data: T): T {
|
||||
const out: any = { ...data };
|
||||
if (typeof out.name === 'string') out.name = stripHtml(out.name);
|
||||
if (typeof out.usernameFieldName === 'string') out.usernameFieldName = stripHtml(out.usernameFieldName);
|
||||
if (typeof out.passwordFieldName === 'string') out.passwordFieldName = stripHtml(out.passwordFieldName);
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function createProvider(data: {
|
||||
name: string;
|
||||
portalUrl?: string;
|
||||
usernameFieldName?: string;
|
||||
passwordFieldName?: string;
|
||||
}) {
|
||||
const portalUrl = assertValidPortalUrl(data.portalUrl);
|
||||
const clean = stripProviderStrings(data);
|
||||
const portalUrl = assertValidPortalUrl(clean.portalUrl);
|
||||
return prisma.provider.create({
|
||||
data: {
|
||||
...data,
|
||||
...clean,
|
||||
portalUrl,
|
||||
isActive: true,
|
||||
},
|
||||
@@ -75,7 +89,7 @@ export async function updateProvider(
|
||||
// portalUrl nur validieren wenn explizit mitgesendet (undefined = unverändert).
|
||||
// Leerstring = "auf null setzen" - hier setzen wir explizit auf null,
|
||||
// damit Prisma nicht den alten Wert hält.
|
||||
const updateData: typeof data = { ...data };
|
||||
const updateData: typeof data = stripProviderStrings(data);
|
||||
if (data.portalUrl !== undefined) {
|
||||
const validated = assertValidPortalUrl(data.portalUrl);
|
||||
(updateData as { portalUrl: string | null }).portalUrl = validated ?? null;
|
||||
|
||||
Reference in New Issue
Block a user