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>
25 lines
919 B
TypeScript
25 lines
919 B
TypeScript
/**
|
||
* Defense-in-depth: liefert einen URL-String nur zurück, wenn er ein
|
||
* sicheres http(s)-Schema hat. Sonst undefined.
|
||
*
|
||
* Hintergrund: das Backend validiert beim Speichern (Pentest 46.1),
|
||
* aber Alt-Daten in der DB können noch `javascript:alert(...)` o.ä.
|
||
* enthalten. React eskapt URLs in `href` NICHT automatisch – ein Klick
|
||
* auf einen `javascript:`-Link triggert die XSS im User-Browser.
|
||
*
|
||
* Diese Funktion wird überall dort eingesetzt, wo wir User-Input
|
||
* als `<a href>` rendern oder per `window.open` öffnen.
|
||
*/
|
||
export function safeHttpUrl(value: string | null | undefined): string | undefined {
|
||
if (!value) return undefined;
|
||
const trimmed = value.trim();
|
||
if (trimmed === '') return undefined;
|
||
try {
|
||
const parsed = new URL(trimmed);
|
||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return undefined;
|
||
return trimmed;
|
||
} catch {
|
||
return undefined;
|
||
}
|
||
}
|