Viele haben sich schon zu unterschiedlichen Aspekten des aktuell in der Sonne der Aufmerksamkeit stehenden Log4j-Remote-Code-Execution-Problems-durch-clever-formulierten-User-Input-der-geloggt-wird geäußert. Mir fällt dabei vor allem auf, dass die meisten Kommentatoren zwar kleine Teilaspekte des zugrundeliegenden Problems meist korrekt beschreiben, es aber an abwägendem sowohl-als-auch eher mangelt. Denn m.E. ist diese Misere keins von den Problemen, die eine einfache Lösung haben.
Ich kann mit vielen der bereits von Anderen genannten Teilaspekte mitgehen. Ja, in Java kann man z.B. durch fortgeschrittene Featuritis und Libraritis und Frameworkitis sehr komplexe Lösungen bauen, die keiner mehr durchschaut. Aber das geht in praktisch jeder Programmiersprache, außer in denen, die schon unhandlich werden, sobald man versucht mäßig komplexe Probleme zu lösen. Hier ein Java-Spezifikum zu sehen ist komplett absurd. Ja, 3rd-party-Abhängigkeiten die man nicht durchverstanden hat sind potenziell gefährlich. Aber selbstgebaute Lösungen sind ebenfalls gefährlich. Ja, der Zeitdruck und die inadäquate Bezahlung in vielen Softwareprojekten ist eine der vielen Problemursachen. Aber auch ohne Zeitdruck und mit unendlichen Mitteln können Katastrophen programmiert werden. Ja, man sollte danach streben, möglichst einfache Lösungen zu bauen. Aber einfache Lösungen, die gleichzeitig problemadäquat sind, fallen auch nicht vom Himmel und brauchen Zeit und Geld – Vereinfachung ohne es zu einfach zu machen ist eine hohe Kunst.
Beim gerade aktuellen Problem kann man es sich einfach machen und sagen “was kann an Logging schon so schwer sein”. Ja dann nehmt doch einfach System.out.println, wenn das ausreichend ist. Kurze Zeit später werdet ihr feststellen, dass es ganz nützlich wäre, mehrere Logkanäle zu bedienen. Den Loglevel von außen konfigurierbar zu machen. Beim Schreiben von Logfiles jedes einzelne File nicht zu groß werden zu lassen. Auf Effizienz beim Loggen achten, also am besten asynchron arbeiten. Und die Log-Datenbank xy anbinden. Für das Loganalysetool der Wahl den Timestamp der Logmeldung parametrierbar zu machen. Ich glaube nicht, dass eine signifikante Anzahl der Log4j-Features “einfach so” eingebaut wurden, da steckt schon jeweils ein valider Usecase dahinter. Und es hat seinen Grund, warum es nicht nur Log4j gibt, sondern auch JUL oder Logback oder tinylog – oder der gute alte IBM LogManager, falls den noch jemand kennt. Wer die Features eines Log4j nicht will oder braucht, versteckt sich hinter slf4j und überlässt dem Kunden die Auswahl der finalen Logimplementierung – aber öffnet gleichzeitig einen interessanten neuen Angriffsvektor, wenn Schadcode beim Deployment als slf4j-implementierendes Jar eingeschleust wird. Und wenn ich dann nach aufwändiger Analyse zum Schluss komme, dass Logback alle meine Wünsche erfüllt, kommt morgen eine neue Anforderung um die Ecke, die nur durch Log4j abgebildet werden kann – was dann? Anforderung ablehnen? Lib wechseln? Von vorne anfangen?
Man soll sich keinen Illusionen hingeben. Was bei Log4j passiert ist, kann mit jeder Programmiersprache, auf jedem Betriebssystem, ja sogar auf jeder CPU passieren. Spectre konnte über JavaScript im Browser ausgenutzt werden – noch so viele Schichten zwischen Quellcode und Ausführung haben uns nicht vor den Auswirkungen eines CPU-Bugs geschützt. Schon im guten alten Z80 gab es “illegale” Opcodes, die trotzdem etwas Vernünftiges gemacht haben, als Seiteneffekt der Implementierung. Nur 8500 Transistoren, und trotzdem ein solcher “Bug” (oder “Feature”, je nachdem wen man fragt). Der aktuelle Apple M1 Max besteht aus 57 Milliarden Transistoren. Natürlich nicht direkt vergleichbar, weil jede Menge Cache damit realisiert ist, aber auch durch Caches können ja interessante Effekte entstehen (Stichwort Seitenkanalangriff). Jedenfalls ein geeignetes Beispiel für die immens gewachsene Komplexität auf allen beteiligten Ebenen.
Ich bin auch ein Fan davon, mit möglichst wenigen Abhängigkeiten in meinen Java-Anwendungen auszukommen. Aber diese Strategie trägt eben auch nicht unendlich weit. Beispiel: wenn Persistenzierung in einer Datenbank gefragt ist, kann man von Hand JDBC machen oder Hibernate verwenden oder JPA oder JDO oder was auch immer das Framework der Wahl so mitbringt. Oder man speichert seine Daten in einer simplen Datei. Oder doch über BerkeleyDB oder SQLite? Wenn die Anforderung lautet, einen Webservice zu schreiben, beginnt man dann mit seinem eigenen Server und seiner eigenen Security-Schicht, oder nimmt man doch was von der Stange und vertraut darauf, dass die Profis es entwickelt haben? Ist eine Bibliothek, die hundert unnütze Funktionen zusätzlich zu den für mich nützlichen zehn bietet, denn notwendigerweise die falsche Wahl, wenn die Konkurrenzbibliothek mit nur zwanzig unnützen Funktionen z.B. closed source ist oder viel weniger Nutzer hat oder eine viel kleinere Community? Oder alles drei? Ist es wirklich sinnvoll, sich auf den Featureumfang der Java-Plattform zu beschränken und sklavisch auf 3rd-party-Libs zu verzichten – ist denn sichergestellt, dass die “Bordmittel” wirklich von höherer Qualität sind als Drittbibliotheken?
Auf Basis von Post-hoc-Erkenntnissen klug daher schwätzen (“hättet ihr mal nicht Log4j verwendet” – ersetze Log4j durch “Intel-CPU” oder “OpenSSL” oder “PHP” oder “Linux” oder “Docker” oder “IIS” oder “Applets”) kann jeder. Aber was ist die “richtige Strategie” im Angesicht des Ungewissen, wenn man vor einer erheblichen zu implementierenden Komplexität steht, die nicht mal schnell ein Profi-Entwickler im Alleingang und minimalen Abhängigkeiten hindengelt (und dieser Profi dann auch dauerhaft für die Wartung zur Verfügung stehen muss)? Es wird letztlich immer eine Abwägungsfrage bleiben. Tritt kein Problem auf, war die Abwägung tendenziell richtig – oder man hat einfach nur Glück gehabt.
Vermutlich ist das die einzige vielversprechend Strategie. Einfach Glück haben.
Da fällt mir ein: schon 2015 habe ich unter der schönen Wortschöpfung “Abstraktionskaskade” diese Problematik beleuchtet. Und schon damals war ich nicht der erste.