Blog ·
Ein Scan, der seinen eigenen Takt schloss
Jeder erfolgreiche geplante Scan setzte sein eigenes Zeitfenster auf abgelaufen, wegen eines Wortes aus der falschen Aufzählung. Zwei Defekte, zwei Messungen.
Von Provibr
Unser Expositionsscan hat einen Planer. Er beansprucht ein Zeitfenster, schickt einen Scanbefehl an den Agent, der die Maschine erreichen kann, und wartet auf zwei Dinge: die Messung selbst und den Abschluss des Befehls, der sie trug. Endet ein Zeitfenster ohne Antwort, gilt es als abgelaufen, und die Maschine erscheint als „keine aktuelle Messung“.
Jeder erfolgreiche Scan setzte sein eigenes Zeitfenster auf abgelaufen. Vier von vier, gemessen gegen einen echten Agent, während die vier Messungen selbst sauber eingelesen und gespeichert wurden. Das Panel meldete, vier Maschinen seien nicht kürzlich gescannt worden, Minuten nachdem sie gescannt worden waren.
Ein Wort, aus einer anderen Tabelle
Die Behandlung, die ein unbeantwortetes Zeitfenster schließt, begann mit einem frühen Ausstieg: War der Befehl erfolgreich, gibt es nichts zu schließen, also verlassen.
Das Wort, mit dem sie verglich, war succeeded. Dieses Wort gibt es in der Aufzählung der Befehlszustände nicht, die pending, sent, completed, failed, timeout und cancelled kennt. Es ist das Wort aus einer ganz anderen Tabelle, jener für Agent-Installationen, und es landete hier, weil jemand die Behandlung mit den Zuständen jener anderen Tabelle vor Augen schrieb und weil der Parameter als schlichter Text typisiert war.
Schlichter Text ist die ganze Geschichte. Der Vergleich übersetzt sich gegen jeden Text, also widerspricht nichts, und das Versagen ist keine Ausnahme, sondern ein Zweig, der nie genommen wird. Die Behandlung lief ihren Pfad für „es kam keine Antwort“ gegen jeden erfolgreichen Scan und setzte gehorsam jedes Zeitfenster auf abgelaufen.
Kein Modultest hätte das finden können. In einer Nachbildung gibt es keinen Befehlslebenszyklus: Die Nachbildung ist das, was den Zustand liefert, also ist der Zustand, was der Testautor aufschrieb, und der schrieb dasselbe falsche Wort. Nur ein echter Agent, der echte Abschlüsse durch den echten Verteiler schickt, erzeugt den Zustand, der es freilegt. Der Parameter ist jetzt auf die drei Zustände typisiert, die diese Funktion wirklich erreichen können, so wie die entsprechende Monitoring-Behandlung es immer schon tat. Zwei Funktionen, dieselbe Frage, und nur eine davon war sorgfältig geschrieben.
Und die zwei Nachrichten treffen in falscher Reihenfolge ein
Den Vergleich zu reparieren reichte nicht, und der Grund ist eine zweite Sache, die nur ein echter Agent zeigt.
Der Agent sendet zwei getrennte Nachrichten: das Scanergebnis und den Abschluss des Befehls. Sie reisen unabhängig. Gemessen kam das Ergebnis zuerst an, vierzehn Millisekunden vor dem Abschluss. Dieser Abstand ist klein genug, dass niemand darüber nachdenkt, und groß genug, dass er immer auftritt.
Wenn die Abschlussbehandlung läuft, ist das Zeitfenster also meist schon vom Ergebnis geschlossen. Eine Aktualisierung ohne Bedingung öffnet dann ein beendetes Zeitfenster erneut und setzt es abermals auf abgelaufen, was der ursprüngliche Fehler mit einem anderen Hut ist. Die Aktualisierung ist deshalb auf die beiden Zustände beschränkt, in denen ein Zeitfenster noch auf Antwort wartet. Ist es bereits entschieden, ändert die Behandlung nichts.
Das ist eine allgemeine Form: Kann eine Tatsache aus zwei Richtungen eintreffen, muss der zweitrangige Schreiber benennen, aus welchen Zuständen er sich entfernen darf.
Ein geschlossener Befund, der die ganze Flotte anhielt
Der zweite Defekt ist leiser und richtete mehr Schaden an.
Aktualisiert sich ein Schwachstellenverzeichnis, werden offene Befunde gegen die neuen Daten neu bewertet. Ein Befund, dessen Übereinstimmung verschwunden ist, wird geschlossen, und zwar als unbestätigt und nicht als behoben, denn es wurde nichts gemessen: Geändert hat sich unsere Ableitung, nicht die Maschine. Dieser Unterschied ist der Grund, warum es diesen Zustand gibt.
Die Neubewertung holte die bestehenden abgeleiteten Befunde, um „diesen aktualisieren“ von „einen neuen anlegen“ zu unterscheiden. Sie holte nur die offenen. Das ist ein vernünftig aussehender Filter und er ist falsch, denn die Eindeutigkeitsbedingung darunter weiß nichts von Zuständen: Sie steht auf dem Server und einem Fingerabdruck des Befundes, Punkt.
Die Abfolge geht also so: Ein nationales Schwachstellenverzeichnis korrigiert einen Versionsbereich, unsere Übereinstimmung verschwindet, der Befund wird geschlossen. Wochen später zieht dasselbe Verzeichnis den Bereich zurück, was tatsächlich vorkommt, und die Übereinstimmung kehrt wieder. Die Abfrage sieht die geschlossene Zeile nicht. Der Code nimmt den Zweig „das gibt es noch nicht“. Das Einfügen trifft den eindeutigen Index und wirft einen Bedingungsverstoß.
Und dann entkommt dieser Verstoß. Der Aufrufer fängt ihn als „die Advisory-Runde ist gescheitert“, und diese Runde bewertet danach gar nichts mehr neu, für jeden Kunden der Plattform. Der Zustand, der ihn verursacht hat, verschwindet nicht von selbst, also scheiterte jede weitere Runde auf dieselbe Weise. Ein Befund, der geschlossen und wieder geöffnet worden war, fror die Neubewertung einer ganzen Flotte ein, dauerhaft, mit einer Protokollzeile als einzigem Symptom.
Die Reparatur ist klein: die geschlossenen Zeilen mitholen, und passt ein geschlossener Befund erneut, ihn mit unversehrter Zeitleiste wieder öffnen statt einen neuen zu beginnen. Ein wieder geöffneter Befund ist eine andere Tatsache als ein neuer, und wer ihn bewertet, verdient zu sehen, dass dies zurückkam und nicht ankam.
Eine Feed-Schleife, dreimal gemessen
Der Feed-Arbeiter entstand, bevor es eine Möglichkeit gab, ihn ohne eine echte Datenbank laufen zu lassen, und das ist die Falle, in welche die vorige Sicherheitsrunde geriet: Das Datenbankpaket zu laden liest die Umgebung und verbindet, also schrieb ein Modultest mit eingesetzter Abrufroutine still vier Advisories in eine Entwicklungsdatenbank, während er grün meldete.
Die Speicherseite ist jetzt eine Naht, und die Schleife lässt sich gegen eine Buchführung im Speicher prüfen, während der Produktionscode die echte unverändert nutzt. Danach lief sie dreimal gegen die tatsächlichen öffentlichen Feeds.
| Runde | Anfragen | Was geschah | Zeit |
|---|---|---|---|
| Kalt | 4 | 7.066 Advisories gespeichert, 31,4 MB nach Entpacken | 18,7 s |
| Unverändert | 1 | 304 Not Modified, gespeicherte Prüfwerte behalten, null Schreibvorgänge | 0,3 s |
| Zeiger zwei Tage zurück | 3 | 355 Advisories aktualisiert, keine neuen | Sekunden |
Die 304 ist der Sinn der Übung. Ein statisch ausgelieferter Advisory-Katalog beantwortet eine bedingte Anfrage mit „nichts geändert“, und das richtige Verhalten ist, die gespeicherten Prüfwerte zu behalten, nichts zu schreiben und die Runde als Erfolg zu zählen.
Ein paar Größen sind es wert, sie zu kennen, denn genau dort wird geraten: Der Katalog der aktiv ausgenutzten Schwachstellen ist auf der Leitung etwa 188 kB groß, weil er komprimiert ist, und eine Seite mit zweitausend Einträgen aus dem größeren Feed sind rund 18,7 MB. Beide werden über eine harte Byteobergrenze gelesen und nicht über die gewöhnliche JSON-Hilfe, die überhaupt keine Grenze kennt; dies sind die beiden größten Antworten, welche die Plattform aufnimmt, und sie kommen von Rechnern, die wir nicht betreiben.
Ein Fenster, das halbiert statt überspringt
Der größere Feed ist seitenweise abrufbar, innerhalb eines wandernden Zeitfensters. Zwei Dinge waren an dieser Schleife falsch, und beide verloren Daten, ohne je etwas zu melden.
Enthielt ein Fenster mehr Einträge, als die Seitenobergrenze zuließ, hielt die Schleife an und schob den Zeiger dennoch ans Ende des Fensters. Alles jenseits der Obergrenze wurde still übersprungen und wäre nur dann je aufgegriffen worden, wenn eine andere Änderung dieselben Einträge erneut berührt hätte.
Und eine leere Seite mitten in einem Fenster, während es weitere Einträge geben sollte, schob den Zeiger eine volle Seite weiter. Zweitausend Einträge fort.
Beides wird nun nach demselben Grundsatz behandelt: Passt die Antwort nicht, eine kleinere Frage stellen, und ist die Antwort unmöglich, nichts bewegen.
Ein zu großes Fenster wird halbiert statt abgeschnitten, höchstens sechsmal, bis zu einem Boden von einer Stunde. Sechs Halbierungen bringen eine Woche auf gut zwei Stunden, was für jeden realistischen Rückstand genügt. Unter einer Stunde hört es auf: Ein Fenster von Minuten, das noch immer zwanzigtausend Einträge liefert, ist kein Fensterproblem, sondern eine Störung, und nicht zu schreiben und den Zeiger stehen zu lassen ist dann zufällig die einzige ehrliche Antwort. Eine leere Seite mitten im Fenster wird genauso behandelt, als Fehler, der den Zeiger in Ruhe lässt, damit die nächste Runde es erneut versucht statt über eine Lücke hinwegzugehen.
Die Halbierung geschieht nach dem Schreiben der Seite, nicht davor. Was wir bereits gelesen haben, gehört zum ursprünglichen Fenster und ist gültig; es wegzuwerfen tauschte eine Art Datenverlust gegen eine andere.
Eine Kleinigkeit aus derselben Ecke, die Wiederholung wert, weil sie unter Zeitdruck leicht misslingt: Der Zugriffsschlüssel gehört in einen Header und niemals in die Abfragezeichenfolge. URLs landen in Protokollen, in Zwischenspeichern und in Fehlerberichten. Header nicht.
Was wir aufschreiben würden
- Den Parameter typisieren, erst recht bei einem Zustand. Ein Textvergleich mit einem Wort aus der Aufzählung einer anderen Tabelle übersetzt sich, läuft und nimmt für immer den falschen Zweig.
- Zwei Nachrichten über ein Ereignis treffen in falscher Reihenfolge ein. Den zweitrangigen Schreiber auf die Zustände beschränken, die er verlassen darf.
- Eine Eindeutigkeitsbedingung, die Zustände übergeht, trifft auf Code, der es nicht tut. Die geschlossenen Zeilen mitholen, sonst wird die erste Wiedereröffnung ein Verstoß.
- Fragen, was ein einzelner Fehler vergiftet. Eine Ausnahme, die aus einer Schleife je Element entkommt, macht aus einer schlechten Zeile einen plattformweiten Stillstand, und der Zustand klärt sich nicht von selbst.
- Eine bedingte Anfrage mit der Antwort 304 ist ein Erfolg. Die Prüfwerte behalten, nichts schreiben und es nicht als Fehlschlag zählen.
- Ist ein Fenster zu groß, halbieren; ist eine Seite unmöglich, anhalten. Einen Zeiger über ungelesene Daten zu schieben ist der einzige Ausgang ohne Rückweg.
- Ein Arbeiter, der ohne Datenbank nicht läuft, wird gegen Ihre Datenbank geprüft. Eine Speichernaht ist billiger als das Aufräumen.
Zur Klarheit: Der Expositionsscan ist gebaut und steht hinter einem Ausrollschalter, kein Kunde hat ihn also vor sich. Die Neubewertung, die Feeds und die Verteilung auf mehrere Adressen sind ausgeübt; unbewiesen bleiben die Fensterverengung gegen einen lebenden statt eingesetzten Feed, das Indexverhalten auf einem wirklich vollen Katalog und ein Scan von einer anderen Maschine als jener, die ihn anforderte.
Aus der Plattformarbeit an Provibr. Warum der Abgleich sich weigert, einer Version sicher zu sein, steht in ein Treffer, der sich weigert sicher zu sein, und warum die Sonde dahinter fast nichts sendet, steht in warum ein Schwachstellenscanner null Bytes senden sollte.