Pentest 43.6 MEDIUM + 43.5 INFO: History-XSS + blocked:-Marker

43.6 MEDIUM: ContractHistoryEntry.title + .description waren auf
beiden Pfaden ungestrippt – Admin konnte HTML/Script-Tags
einschreiben, Portal-User las sie roh zurück. Fix: stripHtml()
auf Create + Update (Write-Pfad) und sanitizeEntry() im List +
Get (Read-Pfad), damit Alt-Daten ebenfalls clean rausgehen.

43.5 INFO: stripHtml ersetzt javascript: -> blocked: – sinnvoll
bei URL-Feldern, hässlich in Tarif-/Preis-Namen ("blocked:alert(1)"
als Preis). Neuer stripForDisplay-Wrapper entfernt den Marker
zusätzlich in CONTRACT_DISPLAY_STRING_FIELDS + CUSTOMER_DISPLAY_
STRING_FIELDS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 19:58:20 +02:00
parent b9a6d99d50
commit 83f1984f12
2 changed files with 39 additions and 8 deletions
@@ -1,4 +1,5 @@
import prisma from '../lib/prisma.js';
import { stripHtml } from '../utils/sanitize.js';
export interface CreateHistoryEntryData {
title: string;
@@ -7,23 +8,36 @@ export interface CreateHistoryEntryData {
createdBy: string;
}
// Read-Time-Defensive: title + description durch stripHtml schicken, damit
// Alt-Einträge (vor Pentest 43.6) mit rohen HTML-Payloads nicht roh
// rausgehen. Schützt zusätzlich gegen einen umgangenen Write-Filter.
function sanitizeEntry<T extends { title: string; description: string | null }>(entry: T): T {
return {
...entry,
title: stripHtml(entry.title) as string,
description: entry.description != null ? (stripHtml(entry.description) as string) : entry.description,
};
}
/**
* Alle Historie-Einträge für einen Vertrag abrufen
*/
export async function getHistoryEntries(contractId: number) {
return prisma.contractHistoryEntry.findMany({
const entries = await prisma.contractHistoryEntry.findMany({
where: { contractId },
orderBy: { createdAt: 'desc' },
});
return entries.map(sanitizeEntry);
}
/**
* Einzelnen Historie-Eintrag abrufen
*/
export async function getHistoryEntry(contractId: number, entryId: number) {
return prisma.contractHistoryEntry.findFirst({
const entry = await prisma.contractHistoryEntry.findFirst({
where: { id: entryId, contractId },
});
return entry ? sanitizeEntry(entry) : null;
}
/**
@@ -39,11 +53,14 @@ export async function createHistoryEntry(contractId: number, data: CreateHistory
throw new Error('Vertrag nicht gefunden');
}
// Pentest 2026-05-30 (MEDIUM, 43.6): Admin konnte HTML-Tags in title +
// description schreiben, Portal-User las sie roh zurück. stripHtml räumt
// Tags + gefährliche URI-Schemata vor dem Persistieren weg.
return prisma.contractHistoryEntry.create({
data: {
contractId,
title: data.title,
description: data.description,
title: stripHtml(data.title) as string,
description: data.description != null ? (stripHtml(data.description) as string) : data.description,
isAutomatic: data.isAutomatic ?? false,
createdBy: data.createdBy,
},
@@ -73,8 +90,8 @@ export async function updateHistoryEntry(
return prisma.contractHistoryEntry.update({
where: { id: entryId },
data: {
title: data.title,
description: data.description,
title: data.title != null ? (stripHtml(data.title) as string) : undefined,
description: data.description != null ? (stripHtml(data.description) as string) : undefined,
},
});
}