Auf dieser Seite

Öffnen Sie einen beliebigen SAST-Report zu einer laufenden Codebasis. Sie werden vierstellige Zahlen neben dem Wort „kritisch" sehen. Öffnen Sie denselben Scan eine Woche später: dieselben Zahlen, plus minus. Irgendwann ertrinkt ein Security Engineer im Triage und das Team liefert trotzdem aus. Dieser Beitrag handelt davon, was in diesen Reports tatsächlich passiert, und warum das meiste, was Sie sehen, nicht das ist, was es behauptet zu sein.
Wenn ein SAST-Werkzeug zum ersten Mal „1.247 Schwachstellen" sagt, stellt eine kleine Stimme die richtige Frage: Gibt es in diesem Repository wirklich eintausendzweihundertsiebenundvierzig ausnutzbare Bugs? Sie wissen schon, dass die Antwort nein ist. Was Sie vielleicht nicht wissen, ist genau warum die Antwort nein ist, und was wahr sein müsste, damit die Zahl etwas bedeutet.
Es geht hier um drei Filter: Erreichbarkeit, Ausnutzbarkeit und Geschäftslogik. Jedes statische Analysewerkzeug, das seinen Lauf wert ist, muss sie in irgendeiner Reihenfolge anwenden. Die meisten tricksen bei mindestens einem. Hier steht, was jeder Filter tut, mit konkretem Code, und warum reines Pattern Matching noch ein Jahr später vierstellige Reports produziert, obwohl ein vierstelliger Report jedes Team zum Aufgeben bringt.
Die 1.200 sind kein SAST-Bug. Sie sind die Konstruktion.
Ein pattern-basierter SAST-Scanner tut ungefähr das: Quelle parsen, den AST durchlaufen und für jeden Knoten prüfen, ob er einer Liste gefährlicher Formen entspricht. eval(x). exec(x). Ein SQL-String mit + zwischen Literalen. Eine Zuweisung an innerHTML. Jeder Treffer wird ein Befund.
Das ist schnell und sprachübergreifend portierbar. Es hat auch nichts damit zu tun, ob überhaupt jemand die gefährliche Form auslösen kann. Wenn der Scanner 1.247 Befunde meldet, sagt er in Wahrheit: „ich habe 1.247 syntaktische Muster gefunden, die im Prinzip in irgendeinem Aufrufkontext gefährlich sein könnten, den ich mir nicht angesehen habe."
Dieser letzte Nebensatz trägt die ganze Last. Nehmen wir ihn auseinander.
Filter eins: Erreichbarkeit
Ein Befund ist erreichbar, wenn ein Angreifer den Code, der ihn enthält, tatsächlich ausführen kann.
Sehen Sie sich diesen Express-Handler an.
app.post("/admin/migrate", requireAdmin, async (req, res) => {
const { sql } = req.body;
const result = await db.raw(sql); // SAST flags this
res.json(result);
});
Ein Pattern-Scanner markiert die Zeile db.raw(sql). SQL-Injection aus Nutzereingaben. Schweregrad: kritisch. Über die Form irrt der Scanner nicht; das ist die Form einer SQL-Injection. Aber um sie auszunutzen, muss der Request an requireAdmin vorbei. Das heißt, der Angreifer muss bereits Admin sein. Das heißt, hier liegt keine SQL-Injection vor, sondern ein „wir vertrauen darauf, dass Admins die Datenbank nicht zerlegen"-Problem, und das ist ein anderes Gespräch.
Erreichbarkeit fragt: Können Daten von einem beliebigen von außen steuerbaren Eintrittspunkt (ein Request, eine Queue-Nachricht, ein deserialisiertes Payload) bis zu dieser gefährlichen Form gelangen? Lautet die Antwort nein, ist der Befund nicht echt, auch wenn das syntaktische Muster echt ist.
Es gibt drei übliche Gründe, warum Code in diesem Sinne unerreichbar ist:
- Authentifizierungs- oder Autorisierungstore liegen zwischen Eintrittspunkt und Sink. Die Middleware kappt den gefährlichen Pfad.
- Bereinigung verwandelt die Angreifereingabe, bevor sie den Sink erreicht. Ein
parseInt, das an Nicht-Ziffern scheitert. Eine WHERE-Klausel aus einer parametrisierten Query. Ein Framework mit automatischem HTML-Escaping. - Die Funktion wird nie aufgerufen, von keinem erreichbaren Pfad. Toter Code ist eine reale Kategorie und moderne Codebasen sind voll davon. Altes Admin-Werkzeug. Ein Handler auf einer Route, die es nicht mehr gibt. Eine Bibliotheksfunktion, die nur Tests benutzen.
Die ehrliche Fassung von „1.247 Befunden" klingt eher wie „davon 380 erreichbar von mindestens einem von außen steuerbaren Eintrittspunkt". Sie haben die Warteschlange schon um zwei Drittel gekürzt und die Ausnutzbarkeit noch nicht einmal angesehen.
Filter zwei: Ausnutzbarkeit
Erreichbar heißt nicht ausnutzbar. Es heißt nur, dass der Datenpfad existiert.
Nehmen Sie dasselbe db.raw-Beispiel, ohne die Auth-Middleware:
app.post("/api/search", async (req, res) => {
const { q } = req.body;
const result = await db.raw(
"SELECT id, title FROM articles WHERE title LIKE ?",
[`%${q}%`]
);
res.json(result);
});
Das Muster ist noch da. Der Pfad ist noch von jedem unauthentifizierten Client erreichbar. Der Befund lautet noch „SQL-Injection". Aber der gebundene Parameter bewirkt, dass die Datenbank q als Wert und nicht als SQL behandelt. Es gibt keine Injection, ganz gleich was in q steht. Der Befund ist nicht ausnutzbar.
Ausnutzbarkeit betrifft die Form, die Daten zwischen Quelle und Sink annehmen. Drei Dinge zählen:
- Kodierungsgrenzen. Eine WHERE-Klausel mit gebundenen Parametern ist auf der SQL-Ebene exploitsicher. Eine Template-Engine mit Auto-Escaping ist es auf der HTML-Ebene. Pattern-Scanner sehen diese Grenzen nicht immer, weil sie in Framework-Methodenaufrufen leben (
db.raw(template, params)) und nicht in syntaktischen Mustern. - Typverengung. Läuft ein Wert vor dem Sink durch ein
parseInt, ein Joi-Schema oder einen TypeScript Type Guard, ist seine Form eingeschränkt. SQL-Injection verlangt Kontrolle über den Rohstring. Ein typisierter Integer injiziert kein SQL. - Sink-Semantik.
child_process.exec("ls")ist keine Injection, wenn der String ein Literal ist.eval(JSON.stringify(x))ist harmlos, obwohl das Wortevaldarin steht. Der Sink ist gefährlich; der Aufruf nicht.
Nach diesem Filter fällt die Zahl erneut. Von 380 erreichbaren Befunden bleiben vielleicht 60 wirklich ausnutzbare. Die meisten Teams halten hier an, erklären diese 60 zum „echten" Backlog und fangen mit dem Triage an.
Das ist ein Fehler. Die 60, die übrig sind, sind CWE-Bugs. Die Bugs, die weh tun, sind nicht immer CWE-Bugs.
Filter drei: Geschäftslogik
Hier ist eine Schwachstelle, die kein SAST-Scanner per Pattern Matching markieren kann, so raffiniert er auch sei:
app.post("/checkout", requireAuth, async (req, res) => {
const { items } = req.body;
let total = 0;
for (const it of items) {
const product = await db.products.findById(it.productId);
total += product.price * it.quantity; // it.quantity can be -1
}
await charge(req.user, total);
await fulfilOrder(req.user, items);
});
Setzen Sie quantity auf -1, und der Preis wird abgezogen. Stapeln Sie Positionen mit negativer Menge gegen eine positive, und die Summe erreicht null. Das charge(0) geht durch. Die Bestellung wird versandt.
Das steht nicht in der OWASP Top 10. Es ist kein CWE-Muster. Es gibt keine syntaktische Form, die sagt „negative Mengen sind ausnutzbar"; die Form total += a * b ist gewöhnliche Arithmetik. Die Schwachstelle besteht darin, dass das Geschäft negative Mengen in der Eingabe zulässt, während das Geschäft beabsichtigt, dass jede Menge eine positive Ganzzahl ist. Diese Absicht lebt in Ihrer Codebasis, in Produktspezifikationen, in Tests, die niemand geschrieben hat. Sie lebt nicht in einer CWE-Datenbank.
Geschäftslogikfehler stecken hinter fast der Hälfte der Vorfälle, die echten finanziellen Schaden verursachen. Coupon-Stapeln. Rechteausweitung durch Missbrauch eines Ablaufs. Race Conditions bei Zustandsübergängen eines Kontos. Der Scanner, der 1.247 syntaktische Befunde fand, hat jeden einzelnen davon verpasst, weil er auf die Form des Codes und nicht auf seine Absicht schaute.
Um sie zu fangen, brauchen Sie Regeln, die aus der Codebasis selbst gewonnen werden: „dieses Feld ist hier, hier und hier immer eine positive Ganzzahl, aber der Controller validiert es nicht". Und das verlangt, mehr als den AST zu lesen.
Wie kommt man also von 1.200 auf 12?
Die Pipeline, die aus 1.200 zwölf macht, ist keine schlauere Regex. Es ist eine völlig andere Form von Analyse. Grob:
Bauen Sie einen Graphen der Codebasis
Symbole, Funktionsaufrufe, Typflüsse, Framework-Tore, Route-Bindings. Kein flacher Token-Strom. Ein Graph, den der Analysator durchlaufen kann.
Durchlaufen Sie ihn auf Erreichbarkeit und Ausnutzbarkeit
Folgen Sie von jedem Eintrittspunkt aus den Daten. Halten Sie bei Bereinigern, Toren, Typverengungen. Nur Pfade, die den Lauf überleben, sind echte Kandidaten.
Heben Sie Geschäftsregeln aus dem Code selbst
Gewinnen Sie Invarianten aus der Codebasis: dieses Feld ist immer positiv, dieser Übergang läuft immer über eine Zahlung, dieser Ablauf endet immer in einem Audit-Log. Jede Invariante wird zu einer Bedingung, die der Agent (oder der Mensch) erfüllen muss.
Der erste Schritt ist das, was die meisten modernen SAST-Produkte „Code Property Graph" oder Wissensgraph nennen. Ihn zu bauen ist schwer. Ihn zu durchlaufen schwerer. Aber es ist der einzige ehrliche Weg, „ist dieser Befund echt?" zu beantworten, ohne einen Security Engineer Ihre gesamte Codebasis lesen zu lassen.
Die 12, die alle drei Filter überleben, sind die, die ein Jira-Ticket verdienen. Die 1.235, die es nicht taten, sind keine „Fehlalarme" in irgendeinem moralischen Sinn; sie sind syntaktische Muster, die der Scanner ohne Graphen nicht ausschließen konnte. Hat Ihr Scanner keinen Graphen, bekommen Sie jeden Montag alle 1.247.
Warum das 2026 mehr zählt als 2022
2022 war der Report mit 1.247 Befunden ärgerlich. Ein Security Engineer arbeitete ihn langsam ab, Spaß hatte niemand, aber die Codebasis wuchs in menschlichem Tempo und die Warteschlange auch.
2026 wächst die Codebasis im Agententempo. KI-Coding-Agenten schreiben inzwischen einen erheblichen Anteil jeder Zeile, die in Produktion landet. Sie erzeugen Code in Mustern, die der Agent erkennt, nicht in denen, die der Security Engineer erkennt. Die Zahl 1.247 skaliert mit der Schreibgeschwindigkeit, nicht mit der Triage-Geschwindigkeit.
Sie haben zwei Optionen. Entweder wird die Analyse drastisch schlauer (graphbasiert, gefiltert nach Erreichbarkeit, Ausnutzbarkeit und Geschäftslogik, bevor Befunde herausgehen), oder die Warteschlange frisst Sie. Eine dritte Option, in der ein Mensch alles liest, gibt es nicht.
Eine kleine Fußnote
Wir haben CybeDefend um diese Idee gebaut. Die Architektur ist ein Code-Wissensgraph plus eine Schicht, die Geschäftslogik aus dem Code gewinnt (wir nennen sie BLSA), plus eine Inline-Schnittstelle, damit der Analysator läuft, während der Agent schreibt, und nicht nach dem Merge. Wenn Sie bis hierher gelesen haben und dieselben drei Filter an Ihrem eigenen Repository sehen wollen: der installationsfreie Weg ist der schnellste Blick darauf, wie Ihr echtes Backlog aussieht, dreißig Minuten vom Clone bis zum ersten Urteil, ohne Karte.
Die Zahl im Report ist nicht die Zahl der Bugs. Es ist die Zahl der Muster, die der Scanner ohne mehr Arbeit nicht ausschließen konnte. Die Teams, die gewinnen, sind die, deren Scanner diese Arbeit macht.


