Pentest 46.1 HIGH + Info-Konsolidierung: zentrale URL-Validierung
46.1 HIGH (Stored XSS via provider.portalUrl): PUT /api/providers/:id
nahm `javascript:alert(...)` als portalUrl ohne Validierung an, das
Portal rendert es als <a href={portalUrl}> → Klick im Kunden-Browser
löste XSS aus.
Fix: neuer zentraler Helper backend/utils/url.validateHttpUrl
- erlaubt nur http(s)-Schemas (sperrt javascript:, data:, file:,
vbscript:, blob: usw.)
- erfordert absoluten URL mit Host
- per Default keine privaten/Loopback-Hosts (über
isPrivateOrBlockedHost), weil der Wert Endkunden gezeigt wird
- Trailing-Slash wird gestrippt
Eingebaut in:
- provider.service createProvider + updateProvider (HIGH-Fix)
- appSetting.service validateSettingValue für portalLoginUrl
(Refactor der bestehenden ad-hoc Validierung → konsolidiert)
Defense-in-depth Frontend: frontend/utils/url.safeHttpUrl liefert
URLs nur zurück wenn http(s), sonst undefined. Eingesetzt in
ContractDetail bei Portal-Link-Rendering und Auto-Login, damit
Alt-Daten in der DB (vor diesem Fix angelegt) nicht klickbar
bleiben.
INFO-Konsolidierung: damit ist die Schema-/Host-Validierung
einheitlich an einer Stelle. Sanitize-Layer (stripHtml in
sanitize.ts) bleibt für reine Text-Felder zuständig.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,19 @@
|
||||
import prisma from '../lib/prisma.js';
|
||||
import { validateHttpUrl } from '../utils/url.js';
|
||||
|
||||
// Pentest 46.1 (HIGH, 2026-06-01): Stored XSS via provider.portalUrl.
|
||||
// PUT akzeptierte `javascript:alert(...)` als URL, das Portal rendert
|
||||
// sie als <a href={portalUrl}> → ein Klick im Kunden-Browser löst die
|
||||
// XSS aus. Fix: vor Schreiben durch validateHttpUrl, das auch andere
|
||||
// gefährliche Schemata (data:, vbscript:, file:) sperrt und private
|
||||
// IPs verbietet (die URL wird Kunden gezeigt, denen interne Hosts
|
||||
// nichts bringen).
|
||||
function assertValidPortalUrl(portalUrl: string | undefined | null): string | undefined {
|
||||
if (portalUrl == null || portalUrl === '') return undefined;
|
||||
const check = validateHttpUrl(portalUrl, { fieldLabel: 'Portal-URL' });
|
||||
if (!check.ok) throw new Error(check.error);
|
||||
return check.value === '' ? undefined : check.value;
|
||||
}
|
||||
|
||||
export async function getAllProviders(includeInactive = false) {
|
||||
const where = includeInactive ? {} : { isActive: true };
|
||||
@@ -37,9 +52,11 @@ export async function createProvider(data: {
|
||||
usernameFieldName?: string;
|
||||
passwordFieldName?: string;
|
||||
}) {
|
||||
const portalUrl = assertValidPortalUrl(data.portalUrl);
|
||||
return prisma.provider.create({
|
||||
data: {
|
||||
...data,
|
||||
portalUrl,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
@@ -55,9 +72,17 @@ export async function updateProvider(
|
||||
isActive?: boolean;
|
||||
}
|
||||
) {
|
||||
// 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 };
|
||||
if (data.portalUrl !== undefined) {
|
||||
const validated = assertValidPortalUrl(data.portalUrl);
|
||||
(updateData as { portalUrl: string | null }).portalUrl = validated ?? null;
|
||||
}
|
||||
return prisma.provider.update({
|
||||
where: { id },
|
||||
data,
|
||||
data: updateData,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user