Node.js und Logging – Teil 6: Loginhalte filtern (DSGVO)

Logs sind schnell geschrieben – und genauso schnell landen darin sensible Daten. In diesem Artikel zeige ich, wie ich IP-Adressen und E-Mails zentral aus dem Logging entferne, ohne bei jedem Log-Statement neu nachdenken zu müssen. Mit RegEx, einem Winston-Format-Wrapper und einer sauberen Integration in den LogService entsteht eine robuste Lösung für mehr Sicherheit und DSGVO-Bewusstsein.

4 Minuten

Im heutigen Artikel kümmern wir uns darum, sensible Informationen aus unseren Logs fernzuhalten. Meine Erfahrung hat mich gelehrt, beim Logging nicht zu überlegen, ob ich gerade sicherheits- oder DSGVO-relevante Informationen zur Hand habe. Um mir diese mentale Last nicht bei jedem Log-Statement aufzubürden, habe ich entschieden, das Problem im LogService zentral zu lösen.

Für diesen Ansatz werden wir folgende Schritte vornehmen:

  1. Reguläre Ausdrücke für IP- und E-Mail-Adressen definieren.
  2. Winston-Format-Wrapper zum Ersetzen schreiben.
  3. Hilfsmethode in das Winston-Logging einbinden.

Bevor wir loslegen, schau dir die bisherigen Artikel dieser Serie an. Du findest die Links im Überblick zur Artikelserie.

1. Reguläre Ausdrücke definieren

Ich habe es mir angewöhnt, generische Dinge am selben Ort – in diesem Fall im Verzeichnis /src/utils – abzulegen. Dort erstellen wir eine Datei namens RegEx.ts. In unserem Fall wollen wir die IP-Adressen-Versionen 4 und 6 erkennen. Letztere sind extrem komplex. Gleiches gilt für E-Mail-Adressen. Die regulären Ausdrücke sind entsprechend umfangreich, haben sich allerdings im Laufe der Jahre bewährt.

Achtung: Die regulären Ausdrücke sollen mit /g enden, damit sie beim Aufruf von replace() alle Vorkommnisse ersetzen.

export const REGEX_IP4_ADDRESS = /(\d{1,3}\.){3}\d{1,3}/g
export const REGEX_IP6_ADDRESS = /^((?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,7}:|(?:[A-Fa-f0-9]{1,4}:){1,6}:[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,5}(?::[A-Fa-f0-9]{1,4}){1,2}|(?:[A-Fa-f0-9]{1,4}:){1,4}(?::[A-Fa-f0-9]{1,4}){1,3}|(?:[A-Fa-f0-9]{1,4}:){1,3}(?::[A-Fa-f0-9]{1,4}){1,4}|(?:[A-Fa-f0-9]{1,4}:){1,2}(?::[A-Fa-f0-9]{1,4}){1,5}|[A-Fa-f0-9]{1,4}:(?:(?::[A-Fa-f0-9]{1,4}){1,6})|:(?:(?::[A-Fa-f0-9]{1,4}){1,7}|:)|fe80:(?::[A-Fa-f0-9]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(?:25[0-5]|(2[0-4]|1?[0-9])?[0-9])|(?:[A-Fa-f0-9]{1,4}:){1,4}:(?:(?:25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(?:25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/g
export const REGEX_EMAIL = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?<domain>(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\]))/g

2. Winston-Format-Wrapper schreiben

Der Winston-Format-Wrapper ermöglicht den Zugriff auf das Info-Objekt und die Änderung desselben. Um dies zu verdeutlichen, hier die Signatur der Funktion:

TransformFunction = (info: TransformableInfo, opts?: unknown) => TransformableInfo | boolean

Wir bekommen das Info-Objekt vom Typ TransformableInfo hinein und geben es wieder aus. Zwischendurch durchlaufen wir die Metadaten rekursiv und ersetzen dabei die fraglichen Zeichenketten.

type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue } | object  
type Redactable = JsonValue

// This might have performance implications, but logging should not be a bottleneck in normal operations  
// So we accept this trade-off for better security and privacy  
const redactSensitiveData = winston.format((info) => {  
  function redact (value: Redactable): Redactable {  
    if (typeof value === 'string') {  
      return value  
        .replace(REGEX_IP4_ADDRESS, 'xxx.xxx.xxx.xxx')  
        .replace(REGEX_IP6_ADDRESS, 'x:x:x:x:x:x:x:x')  
        .replace(REGEX_EMAIL, 'xxx@$<domain>')  
    }  

    if (Array.isArray(value)) {  
      return value.map(redact)  
    }  

    if (value && typeof value === 'object' && !Buffer.isBuffer(value)) {  
      const obj = value as Record<string, Redactable>  
      const result: Record<string, Redactable> = {
      }  
      for (const [key, val] of Object.entries(obj)) {  
        result[key] = redact(val)  
      }  
      return result  
    }  

    return value  
  }  

  // Redact message  
  if (info.message) {  
    info.message = redact(info.message)  
  }  

  // Redact entire info object except level, message, timestamp  
  const {level, message, ...meta} = info  
  Object.assign(info, redact(meta))  

  return info  
})

3. Hilfsmethode in Logger einbinden

Im LogService definieren wir die Formate abhängig von den Umgebungsvariablen. In den entsprechenden Funktionen wie getDefaultFormat hängen wir den Format-Wrapper als letzten Aufruf vor dem eigentlichen Output-Format. Beispielhaft sieht das wie folgt aus:

winston.format.combine(  
  // ... important - add after attachMetadata
  attachMetadata(),
  redactSensitiveData(), // <- add here
  logFormat,
)

Fazit

Durch die Verwendung des LogServices haben wir die Logik zur Vermeidung des Loggings sensibler Informationen an zentraler Stelle gekapselt. Als Denkanstoss noch eine Idee:

Solltest du API-Tokens in deiner Applikation nutzen, identifiziere deren Format, notiere den dazugehörigen RegEx (z. B. const REGEX_ACCESS_TOKEN = /token_.{10}/g) und ergänze die Redact-Funktion um ein weiteres Replace (z. B. .replace(REGEX_ACCESS_TOKEN, 'token_xxxxxxxxxx').

Im nächsten Artikel greifen wir das Konzept der Correlation-ID auf. Ich erkläre, warum sie hilfreich ist und wie wir die ID in unsere Endpunkte integrieren.

call to action background image

Abonniere meinen Newsletter

Erhalte einmal im Monat Nachrichten aus den Bereichen Softwareentwicklung und Kommunikation gespikt mit Buch- und Linkempfehlungen.