Logging Redaction in .NET und im Browser – Sensible Daten sicher aus Logs entfernen
Logs sind das Gedächtnis einer Anwendung. Sie helfen beim Debuggen, Überwachen und Nachvollziehen von Fehlern in der Produktion. Gleichzeitig können sie zur unbeabsichtigten Fundgrube werden: für Angreifer, die sich Zugang zu deiner Logging-Infrastruktur verschaffen, und für Datenschutzbehörden, die nach einem Vorfall fragen, warum E-Mail-Adressen und Tokens jahrelang unverschlüsselt in deinem Log-Aggregator gespeichert waren.
Dieser Artikel behandelt Redaction, also die gezielte Maskierung oder Löschung sensibler Felder direkt auf dem Logging-Pfad, sowohl serverseitig in .NET als auch im Browser mit LogTape.
Warum Logs zur Datenschutzzeitbombe werden
Die häufigsten Probleme entstehen nicht durch bewusste Fehler, sondern durch bequeme Abkürzungen: Request-Body-Logging für schnelles Debugging, Exception-Dumps, die das komplette User-Objekt serialisieren, HTTP-Logs mit Auth-Headern oder Query-Strings, die Token-Parameter enthalten. Jedes dieser Szenarien kann dazu führen, dass personenbezogene Daten unkontrolliert in dein Logging-Backend fließen.
Die rechtlichen Konsequenzen sind real. DSGVO Art. 5 (Datenschutz-Grundverordnung) verlangt Datensparsamkeit: Es dürfen nur die Daten verarbeitet werden, die für den jeweiligen Zweck notwendig sind. Debugging ist ein legitimer Zweck, aber er rechtfertigt nicht, dass E-Mail-Adressen auf unbestimmte Zeit in der Datenbank liegen. Kommt dann Art. 15 DSGVO ins Spiel, das Auskunftsrecht, wird es aufwendig: Wenn ein Nutzer fragt, welche Daten gespeichert sind, müssen auch Logs durchsucht werden. Das OWASP Logging Cheat Sheet benennt ausdrücklich Passwörter, Tokens, Kreditkartennummern und Personendaten als Daten, die in Logs nichts zu suchen haben.
Hinzu kommt das Angriffsrisiko. Log-Exfiltration ist ein bekannter Angriffspfad, und Log4Shell hat 2021 eindrücklich gezeigt, dass Log-Input direkt als Angriffsfläche missbraucht werden kann. Dabei wurde nicht nur die Log4j-Bibliothek selbst angegriffen, sondern auch die Tatsache, dass viele Anwendungen unkontrolliert Daten in Logs schreiben. Selbst interne Systeme schützen nicht vollständig: Ein Mitarbeiter mit Lesezugriff auf den Log-Index hat unter Umständen Zugang zu Tausenden von Nutzerprofilen.
// Ohne Redaction
{
"timestamp": "2026-03-03T10:00:00Z",
"level": "Information",
"message": "User login",
"email": "max.mustermann@example.com",
"ip": "192.168.1.42",
"authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5c..."
}
// Mit Redaction
{
"timestamp": "2026-03-03T10:00:00Z",
"level": "Information",
"message": "User login",
"email": "***REDACTED***",
"ip": "192.168.*.*",
"authorization": "***REDACTED***"
}
Welche Daten müssen maskiert werden
Bevor du mit der Implementierung beginnst, lohnt sich ein klarer Überblick, welche Datenkategorien grundsätzlich maskiert werden müssen. Zusammenfassend spricht man von PII (Personally Identifiable Information, auf Deutsch: personenbezogene Daten). Eine hilfreiche Einteilung:
- Identifizierende Daten: Vor- und Nachname, E-Mail-Adresse, Telefonnummer, Geburtsdatum
- Offizielle IDs: Personalausweis-Nummer, Steuernummer, Sozialversicherungsnummer, IBAN
- Credentials und Tokens: Passwörter, JWT- und Access-Tokens (JWT: JSON Web Token), API-Keys, Session-Cookies, CSRF-Tokens (CSRF: Cross-Site Request Forgery)
- Netzwerk-Metadaten: IP-Adressen (nach DSGVO als personenbezogene Daten eingestuft), MAC-Adressen, User-Agent in Kombination mit anderen Feldern
- Zahlungsdaten: Kreditkartennummern, CVV, BIC
Bei der Strategie empfiehlt sich ein Allowlist-Ansatz: Statt bekannte sensible Felder zu sperren (Blocklist), legst du fest, welche Felder überhaupt geloggt werden dürfen. Das ist wartbarer und robuster, weil neue Felder nicht automatisch im Log auftauchen. Erstelle gemeinsam mit deinem Team oder dem Datenschutzbeauftragten eine versionierte Liste erlaubter Log-Felder und lege sie im Repository ab.
Serverseitige Redaction in .NET
Microsoft stellt mit Microsoft.Extensions.Compliance.Redaction ein offizielles Paket bereit, das ab .NET 8 der empfohlene Weg ist. Es basiert auf dem IRedactor-Interface und einem IRedactorProvider, der je nach Datenklassifikation den passenden Redactor auswählt.
Zwei vorgefertigte Redactors decken die meisten Szenarien ab: Der ErasingRedactor löscht den Wert vollständig, der HmacRedactor erstellt einen HMAC-SHA256-Hash (HMAC: Hash-based Message Authentication Code) und ermöglicht damit Pseudonymisierung. Letzteres ist besonders nützlich für Audit-Trails, wo du Aktivitäten einem Nutzer zuordnen möchtest, ohne die E-Mail-Adresse im Klartext zu speichern. Für spezifische Anforderungen kannst du eigene Redactors implementieren, die das IRedactor-Interface umsetzen, etwa um nur bestimmte IP-Oktette zu maskieren.
Zunächst benötigst du die NuGet-Pakete:
dotnet add package Microsoft.Extensions.Compliance.Redaction
dotnet add package Microsoft.Extensions.Compliance.Abstractions
Die Registrierung in Program.cs ist übersichtlich:
// Program.cs – Redaction-Registrierung
builder.Services.AddRedaction(redaction =>
{
redaction.SetHmacRedactor(
// Nur zur Veranschaulichung – Key aus Key Vault oder User Secrets laden
options => options.Key = Convert.ToBase64String(
System.Security.Cryptography.RandomNumberGenerator.GetBytes(32)),
new DataClassificationSet(DataTaxonomy.SensitiveData));
// PrivateData-Felder werden vollständig gelöscht
redaction.SetErasingRedactorForHierarchy(DataTaxonomy.PrivateData);
});
builder.Logging.EnableRedaction();
Wichtig: Ohne EnableRedaction() auf dem ILoggingBuilder greifen alle Attribute nicht. Das Paket nutzt Datenklassifikations-Attribute, die du direkt auf Modell-Properties oder Methoden-Parameter setzt:
// Modell-Attributierung
public class LoginRequest
{
[SensitiveData]
public string Email { get; set; } = string.Empty;
[PrivateData]
public string Password { get; set; } = string.Empty;
}
// Logger-Verwendung mit [LoggerMessage] Source Generator
public static partial class AppLogMessages
{
[LoggerMessage(Level = LogLevel.Information, Message = "User login attempt for {Email}")]
public static partial void LogLoginAttempt(this ILogger logger, [SensitiveData] string email);
}
Der [LoggerMessage]-Source-Generator übersetzt die Attribute zur Compile-Zeit in den korrekten Logging-Code. Der entscheidende Vorteil gegenüber Regex-basierter Redaction: kein Runtime-Overhead, keine Fehlmuster und ein Compiler-Fehler, wenn du vergisst, ein Attribut zu setzen. Den HmacRedactor-Key solltest du nicht fest in appsettings.json hinterlegen, sondern aus dem Key Vault oder den User Secrets laden.
Falls dein Projekt bereits intensiv Serilog einsetzt, gibt es als Alternative das Community-Paket serilog-enrichers-sensitive-data, das Regex-basierte Maskierung über WithSensitiveDataMasking() ermöglicht. Der Ansatz ist weniger strikt als Microsoft.Extensions.Compliance.Redaction, weil er keinen Compile-Time-Check bietet, aber für Legacy-Projekte eine pragmatische Einstiegslösung.
Entwicklung vs. Produktion
In der Entwicklung willst du die vollständigen Daten sehen, um Fehler zu verstehen. In der Produktion dürfen sensible Felder nie ungeschützt im Log-Backend landen. Der einfachste Weg, beides zu ermöglichen, ist ein Feature-Flag über die Umgebungskonfiguration:
// appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Debug"
}
},
"Features": {
"EnableRedaction": false
}
}
// Program.cs – Feature-Flag-basierte Aktivierung
if (builder.Configuration.GetValue<bool>("Features:EnableRedaction"))
{
builder.Logging.EnableRedaction();
builder.Services.AddRedaction(/* ... */);
}
Zwei Risiken sind bei der Konfiguration zu beachten: Zu wenig Redaction führt zum PII-Leak, zu viel Redaction macht Debugging unmöglich. Regex-basierte Lösungen haben dabei einen weiteren Nachteil: Sie kosten bei jedem Log-Statement Rechenzeit. Der attributbasierte Ansatz in .NET eliminiert diesen Overhead fast vollständig, weil der Source Generator den Code zur Compile-Zeit generiert.
Frontend-Redaction mit LogTape
Auch Browser-Logs müssen bereinigt werden, bevor sie an Backend-Endpunkte oder Third-Party-Services wie Application Insights, Sentry oder DataDog geschickt werden. Ein wichtiger Hinweis vorab: Client-seitige Redaction ist immer nur eine zusätzliche Schutzmaßnahme, nicht die einzige Schutzmaßnahme. Der Server muss weiterhin als letzte Verteidigungslinie agieren.
LogTape ist ein modernes, Zero-Dependency TypeScript/JavaScript Logging-Framework für Browser und Node.js. Es bietet hierarchische Logger-Kategorien und Custom Sinks, über die du einen Redaction-Layer vor den eigentlichen Remote-Sink schalten kannst:
npm install @logtape/logtape
// logtape.config.ts – Redaction-Sink
import { configure, getConsoleSink } from '@logtape/logtape';
const SENSITIVE_FIELDS = [
'email',
'password',
'token',
'authorization',
'cookie',
] as const;
function redactRecord(
record: Record<string, unknown>,
): Record<string, unknown> {
return Object.fromEntries(
Object.entries(record).map(([key, value]) => [
key,
SENSITIVE_FIELDS.includes(key as (typeof SENSITIVE_FIELDS)[number])
? '***REDACTED***'
: value,
]),
);
}
await configure({
sinks: {
console: getConsoleSink(),
remote: {
// Vor dem Senden an den Server: Redaction anwenden
log(record) {
const redacted = redactRecord(
record.properties as Record<string, unknown>,
);
fetch('/api/logs', {
method: 'POST',
body: JSON.stringify({ ...record, properties: redacted }),
});
},
},
},
loggers: [
{ category: ['app'], sinks: ['console', 'remote'], lowestLevel: 'info' },
],
});
Für unstrukturierte String-Logs und als Fallback-Mechanismus sind Regex-basierte Sanitizer sinnvoll. Ergänzend hilft ein Allowlist-basierter Object-Scrubber, der nur explizit erlaubte Felder weitergibt:
// redact.ts – Utility-Funktionen
const PATTERNS: [RegExp, string][] = [
[/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[EMAIL]'],
[/Bearer\s[\w\-._~+/]+=*/gi, 'Bearer [TOKEN]'],
[/\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g, '[CARD]'],
];
export function sanitizeString(input: string): string {
return PATTERNS.reduce(
(str, [pattern, replacement]) => str.replace(pattern, replacement),
input,
);
}
const ALLOWED_LOG_FIELDS = new Set([
'level',
'message',
'timestamp',
'requestId',
'component',
]);
export function scrubObject(
obj: Record<string, unknown>,
): Record<string, unknown> {
return Object.fromEntries(
Object.entries(obj).filter(([key]) => ALLOWED_LOG_FIELDS.has(key)),
);
}
Für Application Insights kannst du die Telemetry Initializers nutzen, um Telemetrie-Objekte vor dem Senden zu bereinigen.
Falls du Sentry verwendest, bietet der beforeSend-Hook einen praktischen Einstiegspunkt, um Events vor dem Senden zu bereinigen.
End-to-End: React-Frontend und .NET API
Um zu verstehen, wie beide Seiten zusammenwirken, schauen wir uns ein konkretes Szenario an: Ein React-Frontend ruft einen .NET Login-Endpunkt auf. Ohne Redaction sieht das Logging-Backend folgendes:
// Ohne Redaction
// Im Idealfall sendet ihr das Passwort natürlich nicht im Klartext, aber das passiert leider oft genug.
POST /api/auth/login
Body: { "email": "anna@example.com", "password": "MyS3cr3tP@ss!" }
Server-Log: "Login attempt - email=anna@example.com password=MyS3cr3tP@ss!"
Client-Log: { "level": "info", "email": "anna@example.com", "token": "eyJ..." }
Mit aktivierter Redaction auf beiden Seiten sieht das Ergebnis so aus:
// Mit Redaction
Server-Log: "Login attempt - email=[HMAC:a3f9...] password=***"
Client-Log: { "level": "info", "email": "***REDACTED***", "token": "***REDACTED***" }
Auf der Serverseite sorgen [PrivateData]-Attribute am LoginRequest-DTO und der [LoggerMessage]-Source-Generator dafür, dass die Ausgabe bereits beim Logging-Framework bereinigt wird. Auf der Clientseite filtert der LogTape-Sink sensible Properties, bevor der Request an den Log-Endpoint gesendet wird.
Die Kombination ist entscheidend: Weder Client- noch Server-Redaction allein reicht als Schutz aus. Client-seitige Redaction verhindert, dass Logs überhaupt mit sensiblen Daten den Browser verlassen. Server-seitige Redaction stellt sicher, dass auch Logs, die direkt an den Server geschrieben werden, keine PII enthalten. Beide Ebenen zusammen schaffen eine robuste Absicherung.
Test auf allen Ebenen
Redaction-Code ist sicherheitskritisch. Ein unbemerkt deaktivierter Redactor oder eine fehlende Attributierung an einem neuen DTO-Feld, und Passwörter liegen im Klartext in deinem Log-Aggregator. Tests sind daher kein optionales Extra, sondern ein notwendiger Teil des Setups.
Auf der .NET-Seite kannst du mit xUnit und dem FakeLogger sowohl die Attributierung als auch das Verhalten des Redactors direkt testen:
// RedactionTests.cs (xUnit)
public class RedactionTests
{
[Fact]
public void LoginRequest_Password_ShouldBeMarkedAsPrivateData()
{
var property = typeof(LoginRequest).GetProperty(nameof(LoginRequest.Password));
Assert.NotNull(property?.GetCustomAttribute<PrivateDataAttribute>());
}
[Fact]
public void ErasingRedactor_ShouldReturnEmptyString()
{
var redactor = ErasingRedactor.Instance;
var result = new char[10];
var written = redactor.Redact("sensitive".AsSpan(), result);
Assert.Equal(0, written);
}
}
Auf der Frontend-Seite lassen sich die Utility-Funktionen z.B. mit Vitest parametrisiert testen:
// redact.test.ts (Vitest)
import { sanitizeString, scrubObject } from './redact';
describe('sanitizeString', () => {
test.each([
['anna@example.com', '[EMAIL]'],
['Bearer eyJhbGciO...', 'Bearer [TOKEN]'],
['4111 1111 1111 1111', '[CARD]'],
])("redacts '%s' → '%s'", (input, expected) => {
expect(sanitizeString(input)).toContain(expected);
});
});
describe('scrubObject', () => {
it('removes non-allowlisted fields', () => {
const result = scrubObject({
level: 'info',
email: 'x@y.z',
message: 'test',
});
expect(result).not.toHaveProperty('email');
expect(result).toHaveProperty('message');
});
});
Wer es robust haben will, kann trufflehog auch auf Log-Output-Fixtures loslassen, um sicherzustellen, dass keine echten PII in Test-Fixtures landen.
Fazit und Checkliste für sichere Logs
Redaction ist kein Nice-to-Have. Sie ist Teil der Datenschutz-by-Design-Pflicht, die die DSGVO einfordert, und ein aktiver Schutz gegen Log-basierte Angriffspfade. Der attributbasierte Ansatz in .NET ist dabei dem Regex-Ansatz klar überlegen: Er ist schneller, wartbarer und gibt dir zur Compile-Zeit eine Sicherheitsgarantie, die Regex nicht leisten kann. Auf der Client-Seite ist LogTape eine solide Wahl für strukturierte Logs, ergänzt durch Regex-Sanitizer als Fallback für unstrukturierte Meldungen.
Für bestehende Projekte empfiehlt sich ein schrittweiser Rollout: Feature-Flag einführen, DTOs und Logger-Methoden annotieren, Tests schreiben und erst dann in der Produktionskonfiguration aktivieren. Die OpenTelemetry Semantic Conventions bieten außerdem einen nützlichen Referenzrahmen für standardisierte Attribute, die in Observability-Pipelines als sensibel markiert werden können.
Wie gehst du mit PII in Logs um und welche Strategie hat sich bei dir bewährt? Schau gerne bei uns auf LinkedIn vorbei und diskutiere mit.