Artikel

Ökosystem-Review: eine Ticket-Route ohne Wache

Ein zweiter Durchgang, diesmal über alle Projekte statt nur über eins. Der Fund: dieselbe Lücke, die ich sechs Tage vorher behoben hatte, an einer benachbarten Stelle.

BlackZackBlackzack

1045 Wörter · 5 Min. Lesezeit

  • sicherheit
  • yurna
  • audit

Sechs Tage nach dem großen Durchgang habe ich einen zweiten gemacht — diesmal nicht durch ein Projekt, sondern quer über alle: Bot, Dashboard, Verwaltung, Webseite.

Der Anlass war ein Gedanke, der mich nach dem ersten Durchgang nicht losgelassen hat: Wenn der Fehler ein Konsistenzfehler war — eine Stelle, die von den anderen abweicht —, dann ist die Wahrscheinlichkeit hoch, dass es weitere gibt.

Sie war es.

Der Fund

Eine Route, über die Dateien aus Support-Tickets aufgelistet werden. Sie hatte einen Schutz gegen Pfadmanipulation, also gegen den Versuch, mit Verzeichniswechseln aus dem Ablageordner auszubrechen. Sie hatte keine Prüfung, ob der Anfragende auf den zugehörigen Server überhaupt Zugriff hat.

Die Struktur ist dieselbe wie beim Fund sechs Tage vorher — nur eine andere Route im selben Bereich. Die eine hatte ich gefunden, weil sie in der Dokumentation stand. Die andere war später dazugekommen und niemandem aufgefallen.

Die Behebung, und was daran interessant war

Der offensichtliche Teil: Eine Zugriffsprüfung ergänzen. Dazu muss man wissen, um welchen Server es geht — und die Angabe steckte im Pfad, weil die Ablage nach Server, dann nach Ticket gegliedert ist.

Der weniger offensichtliche Teil kam beim Nachdenken über die Ordnerprüfung. Die alte Prüfung stellte sicher, dass der Zielpfad innerhalb des Ablagewurzelverzeichnisses liegt. Das lässt eine Lücke: Ein Verzeichnis, dessen Name mit demselben Präfix beginnt, besteht diese Prüfung ebenfalls.

Konkret: Wenn das Ablageverzeichnis tickets heißt, besteht auch tickets-backup die Prüfung „fängt mit dem Ablageverzeichnis an". Zwei Verzeichnisse nebeneinander, eines geschützt, eines versehentlich zugänglich.

Die Behebung ist eine Kleinigkeit mit Wirkung: Die Prüfung muss auf einer Verzeichnisgrenze enden, nicht auf einem Zeichenvergleich. Und weil die Zugriffsprüfung ohnehin dazukam, wurde die Eingrenzung gleich auf das Unterverzeichnis des jeweiligen Servers verschärft statt auf die Wurzel.

Warnung

Das Muster ist allgemeiner, als es aussieht. Jede Prüfung der Art „liegt A innerhalb von B", die über einen Präfixvergleich läuft, hat dieses Problem — bei Pfaden, bei Adressen, bei Domainnamen. Bei Domains ist es besonders tückisch: Eine Prüfung, ob eine Adresse mit beispiel.de endet, trifft auch auf boesebeispiel.de zu. Die Grenze muss Teil der Prüfung sein.

Was der Durchgang über alle Projekte sonst ergeben hat

Zwei Stellen, an denen ein vollständiges Objekt in eine Antwort geschrieben wurde. Dasselbe Muster wie bei den Sitzungsdaten im Vorjahr: ein Spread über ein Objekt, dessen Inhalt sich später erweitert hat. Beide auf Positivlisten umgestellt.

Eine Prüfung, die im Fehlerfall durchließ. Ein Aufruf an einen externen Dienst mit einem catch, der bei einem Fehler einfach weitermachte — an einer Stelle, an der das Ergebnis über eine Berechtigung entschied.

Mehrere Stellen mit vollständigen Fehlerobjekten in Protokollen. Bekannt, weiterhin offen, aber jetzt gezählt: rund neunzig.

Was ich am Verfahren geändert habe

Nach diesem zweiten Durchgang habe ich meine Vorgehensweise angepasst. Statt Projekt für Projekt gehe ich seitdem nach Mustern durch alle Projekte:

  • Alle Stellen, an denen ein Pfad aus einer Eingabe entsteht.
  • Alle Stellen, an denen ein Objekt vollständig weitergegeben wird.
  • Alle Fehlerbehandlungen um Berechtigungsentscheidungen herum.
  • Alle Vergleiche, die auf Präfixen beruhen.

Das ist deutlich ergiebiger, als sich Angriffsarten vorzustellen. Eine Suche nach dem Muster findet auch die Stelle, an die man beim Nachdenken nie gedacht hätte — weil sie in einem Projekt liegt, an das man gerade nicht denkt.

Die Suche nach Mustern, die daraus geworden ist

Nach diesem zweiten Fund habe ich mein Vorgehen umgestellt — weg von „Projekt für Projekt" hin zu „Muster für Muster". Das sind die Muster, nach denen ich seitdem suche.

Pfade aus Eingaben. Jede Stelle, an der ein Dateipfad aus etwas entsteht, das von außen kommt. Prüfung: Wird die Grenze auf Verzeichnisebene geprüft, nicht auf Zeichenebene?

Objekte, die vollständig weitergegeben werden. Jede Stelle, an der ein Objekt in eine Antwort oder eine Sitzung übernommen wird. Prüfung: Ist es eine Positivliste?

Vergleiche mit Präfixen. Bei Pfaden, Adressen, Domains, Kennungen. Prüfung: Endet der Vergleich an einer Grenze?

Fehlerbehandlungen um Berechtigungsentscheidungen. Prüfung: Was passiert im Fehlerzweig?

Aufrufe an externe Dienste. Prüfung: Kann die Zieladresse von außen beeinflusst werden?

Fünf Muster, und jedes davon lässt sich mit einer Suche über alle Projekte finden. Das ist der Vorteil gegenüber dem Nachdenken über Angriffe: Eine Suche findet auch die Stelle, an die man nicht gedacht hätte.

Warum ich diesen Eintrag geschrieben habe

Es wäre bequemer gewesen, den ersten Durchgang zu veröffentlichen und diesen zweiten Fund für sich zu behalten. Er zeigt, dass ich beim ersten Mal nicht gründlich genug war.

Ich schreibe ihn trotzdem auf, weil die Erkenntnis dahinter wichtiger ist als das gute Bild: Ein Sicherheitsdurchgang ist kein Zustand, sondern eine Tätigkeit. Es gibt kein „geprüft und sauber", es gibt nur „an diesem Tag nach diesen Mustern durchgesehen".

Und das Gefühl, einen Bereich abgehakt zu haben, ist genau das, was die zweite Lücke sechs Tage überleben ließ. Wer nach einem Fund denkt, er kenne den Bereich jetzt, sucht dort nicht weiter — und die zweite Lücke sitzt fast immer neben der ersten.

Wie ich Zugriffsprüfungen heute aufbaue

Aus beiden Funden ist ein Muster geworden, das ich für jede Route mit Datenzugriff anwende.

Die Prüfung hängt am Objekt, nicht an der Route. Nicht „diese Route braucht Rechte", sondern „für diesen Datensatz braucht es diese Berechtigung". Damit ist sie unabhängig davon, über welchen Weg der Zugriff kommt.

Sie steht vor jeder Verarbeitung. Nicht nach dem Laden, nicht nach der Prüfung anderer Dinge.

Sie ist eine Funktion mit einem Namen, nicht eine Bedingung im Code. Eine benannte Funktion kann man suchen — und damit beantworten, welche Stellen sie benutzen und welche nicht.

Der letzte Punkt ist der, der die zweite Lücke gefunden hätte. Eine Suche nach dem Namen der Prüffunktion über alle Routen zeigt sofort, wo sie fehlt. Bei einer inline geschriebenen Bedingung geht das nicht.

Das ist der praktischste Grund, Sicherheitsprüfungen zu benennen: Was einen Namen hat, kann man zählen.

Der unangenehme Teil

Sechs Tage. Zwischen dem Fund, bei dem ich dachte, ich hätte den Bereich verstanden, und dem nächsten Fund derselben Art im selben Bereich lagen sechs Tage.

Das ist die eigentliche Lehre dieses Eintrags, und sie ist unbequem: Ein Durchgang, der etwas findet, ist kein Beweis dafür, dass man fertig ist. Er ist ein Hinweis darauf, wo man weitersuchen sollte. Ich hatte nach dem ersten Fund das Gefühl, den Bereich abgehakt zu haben — und genau dieses Gefühl war der Fehler.