29. Dez. 2025·4 Min. Lesezeit

SQL-Injection in KI-generierten CRUD-Apps: Muster und Behebungen

Lerne, wie man SQL-Injection in KI-generierten CRUD-Apps erkennt — inklusive konkreter verwundbarer Query-Beispiele und sicherer Alternativen mit Parametern und ORM-Features.

SQL-Injection in KI-generierten CRUD-Apps: Muster und Behebungen

Wie SQL-Injection in CRUD-Apps aussieht

SQL-Injection passiert, wenn eine Anwendung Benutzereingaben die Bedeutung einer Datenbankabfrage verändern lässt. Statt als reine Daten behandelt zu werden, wird die Eingabe Teil des SQL. Das kann private Daten offenlegen, Datensätze verändern oder Tabellen löschen.

CRUD-Endpunkte sind häufige Ziele, weil sie ständig Eingaben akzeptieren: Suchfelder, Filter, Edit-Formulare, IDs in URLs und Admin-Oberflächen. Angreifer brauchen nichts Kompliziertes — nur eine Stelle, an der Eingabe in eine Abfrage eingefügt wird.

In vielen KI-generierten CRUD-Prototypen sehen die riskanten Stellen auf den ersten Blick „normal“ aus. Code-Generatoren greifen oft zu schnellem String-Konkatenieren, besonders bei Suche, Filtern und Sortierung. Sie neigen außerdem dazu, Request-Parsing, Query-Erzeugung und Response-Formatting in einer langen Funktion zu kombinieren, sodass eine einzelne unsichere Zeile leicht übersehen wird.

Anzeichen dafür, dass eine CRUD-Route injizierbar sein könnte:

  • SQL-Strings, die mit +, Template-Strings oder String-Replace unter Verwendung von Request-Werten gebaut werden
  • Suchtext, der direkt in LIKE '%...%' eingefügt wird
  • Dynamische ORDER BY- oder Spaltennamen aus Query-Parametern
  • Admin-Endpunkte, die Eingaben vertrauen, weil sie „intern“ sind
  • Fehler, die rohes SQL oder Datenbankdetails anzeigen

Eine einfache Regel hilft: Wenn die Datenbank Benutzereingaben jemals als SQL-Schlüsselwörter interpretieren kann (z. B. OR, UNION, DROP), bist du nicht wirklich abgesichert. Wenn die Datenbank Benutzereingaben nur als Werte sieht, die an Parameter gebunden sind, bewegst du dich in die richtige Richtung.

Häufige verwundbare Muster, die KI-Tools erzeugen

Injection taucht oft an denselben Stellen auf: Suche, Filterung, Sortierung und Pagination. Diese Bereiche fühlen sich wie harmloses String-Handling an, berühren aber bei fast jeder Anfrage die Datenbank.

Ein klassisches Muster ist das Zusammenfügen von Benutzereingaben in eine WHERE-Klausel:

// Vulnerable
const q = req.query.q;
const sql = "SELECT * FROM users WHERE email = '" + q + "'";
await db.query(sql);

Ein weiteres häufiges Muster ist das Anfügen von "optional filters" durch rohe Fragmente in einer Schleife, besonders LIKE '%${q}%' und status = '${status}'. Das Risiko wächst schnell, sobald ein Feld übersehen wird oder mit einer schwachen Ersetzung "gesäubert" wird.

Sortierung und Pagination sind oft Überreste. Man betrachtet WHERE auf Injection und vergisst, dass ORDER BY und LIMIT oft direkt aus Query-Params kommen:

// Vulnerable
const sort = req.query.sort;     // e.g. "created_at DESC"
const limit = req.query.limit;   // e.g. "50"
const sql = `SELECT * FROM orders ORDER BY ${sort} LIMIT ${limit}`;
await db.query(sql);

Du wirst außerdem wiederkehrende Abkürzungen in Prototypen sehen:

  • An manchen Stellen wird escaped, an einer anderen vergessen
  • Beliebige Spaltennamen für "flexibles" Sortieren erlauben
  • IDs als sicher behandeln, weil „sie sind Zahlen“, und sie dann als Strings verwenden
  • Rohes SQL mit Benutzereingaben loggen (kann sensible Daten leaken)
  • Ein unsicheres Query-Muster über viele Routen kopieren

Wenn du eine KI-erstellte App von Tools wie Bolt, v0, Cursor oder Replit geerbt hast, gehe davon aus, dass diese Muster vorhanden sind, bis du das Gegenteil beweist.

Konkretes Beispiel: Rohes SQL mit Parametern fixen

String-Interpolation sieht oft sauber aus, mischt aber Code und Benutzereingaben in derselben Zeichenkette.

Vulnerables Beispiel:

// GET /users?email=...
const email = req.query.email;
const sql = `SELECT id, email FROM users WHERE email = '${email}'`;
const rows = await db.query(sql);

Wenn jemand x' OR '1'='1 übergibt, kann die Abfrage alle Nutzer zurückgeben. Die Lösung ist, den SQL-Text statisch zu halten und Werte separat zu übergeben.

Sichere Alternative: Platzhalter + separate Werte

PostgreSQL-Platzhalter:

const email = req.query.email;
const sql = "SELECT id, email FROM users WHERE email = $1";
const rows = await db.query(sql, [email]);

MySQL/SQLite-Platzhalter:

const sql = "SELECT id, email FROM users WHERE email = ?";
const rows = await db.query(sql, [email]);

Der Kern ist einfach: Eingabe wird nicht in den SQL-String eingefügt. Der Treiber sendet sie als Daten, nicht als Code.

Edge-Cases: IN (...)-Listen und leere Werte

Filter wie „status in [a, b, c]“ werden oft falsch gepatcht.

Unsicher:

const statuses = req.query.statuses; // e.g. "active,paused"
const sql = `SELECT * FROM users WHERE status IN (${statuses})`;

Sicherer: Baue Platzhalter und übergebe die Werte trotzdem separat.

const statuses = (req.query.statuses || "")
  .split(",")
  .map(s => s.trim())
  .filter(Boolean);

if (statuses.length === 0) return res.json([]); // or skip the filter

const placeholders = statuses.map((_, i) => `$${i + 1}`).join(", ");
const sql = `SELECT * FROM users WHERE status IN (${placeholders})`;
const rows = await db.query(sql, statuses);

Praktische Regeln, die unordentliche Regressionen verhindern:

  • Behandle leere Strings als „kein Filter“, nicht als SQL-Text
  • Validiere Typen früh (Zahlen sollten vor der Abfrage Zahlen sein)
  • Akzeptiere niemals rohe SQL-Fragmente aus der Request, auch nicht scheinbar harmlose
  • Halte das Query-Building an einem Ort, damit Fixes greifen

Konkretes Beispiel: Sicheres Filtern ohne string-basiertes SQL

Ein häufiges Injection-Muster sind "optionale Filter", die zu einem wachsenden SQL-String werden.

Vulnerable Form:

// ❌ Vulnerable: string-built WHERE
let where = "WHERE 1=1";
if (q) where += ` AND name ILIKE '%${q}%'`;
if (minPrice) where += ` AND price >= ${minPrice}`;
if (startDate) where += ` AND created_at >= '${startDate}'`;

const sql = `SELECT * FROM products ${where} ORDER BY created_at DESC LIMIT ${limit}`;

Sicheres Muster: Baue Bedingungen separat, halte Benutzereingaben aus dem SQL-Text heraus und übergebe Werte als Parameter.

// ✅ Safe: conditions + params
const conditions = [];
const params = [];

if (q) {
  params.push(`%${q}%`);
  conditions.push(`name ILIKE $${params.length}`);
}

if (minPrice) {
  params.push(Number(minPrice));
  conditions.push(`price >= $${params.length}`);
}

if (startDate) {
  params.push(new Date(startDate));
  conditions.push(`created_at >= $${params.length}`);
}

const whereSql = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
const sql = `SELECT * FROM products ${whereSql} ORDER BY created_at DESC LIMIT 50`;
const rows = await db.query(sql, params);

Einige Details, die subtile Bugs verhindern:

  • Setze %-Wildcards in den Parametern, nicht im SQL-String
  • Parse und valide Daten, binde sie dann als Parameter
  • Konvertiere numerische Bereiche und lehne NaN vor dem Binden ab
  • Wenn ein Filter optional ist, lass die Bedingung ganz weg

Konkretes Beispiel: Sichere Sortierung und Pagination

Prüfe jeden Query-Pfad
Wir prüfen Suche, Filter, Sortierung und Admin-Routen — die Orte, an denen SQL-Injection meist versteckt ist.

Sortierung ist der Ort, an dem sonst "parametrisierte" Endpunkte oft riskant werden. Suchtexte können parametriert werden. Spaltennamen und Sortierrichtungen nicht.

Das sichere Muster: Mappe Benutzereingaben auf eine kleine Allowlist bekannter, sicherer Spalten und Richtungen und lehne alles andere ab.

// Example: Node/Express with Postgres (pg)
const SORT_FIELDS = {
  createdAt: 'created_at',
  email: 'email',
  status: 'status'
};

function buildListUsersQuery({ sort = 'createdAt', dir = 'desc', page = 1, pageSize = 20 }) {
  const field = SORT_FIELDS[sort];
  if (!field) throw new Error('Invalid sort field');

  const direction = String(dir).toLowerCase() === 'asc' ? 'ASC' : 'DESC';

  const limit = Math.min(Math.max(parseInt(pageSize, 10) || 20, 1), 100);
  const offset = Math.max((parseInt(page, 10) || 1) - 1, 0) * limit;

  // Only the allowlisted identifier and direction are interpolated.
  // Pagination values stay parameterized.
  return {
    text: `SELECT id, email, status, created_at FROM users ORDER BY ${field} ${direction} LIMIT $1 OFFSET $2`,
    values: [limit, offset]
  };
}

Schritt-für-Schritt Patch-Workflow für eine bestehende CRUD-App

Wenn eine CRUD-App „größtenteils funktioniert“, versteckt sich Injection oft in den Rändern: Suche, Filter, Sortierung, Admin-Panels und Bulk-Aktionen. Ein Patch-Workflow verhindert, dass man einen Endpunkt fixt und drei andere offen lässt.

  1. Inventarisiere jede Query und jeden Input-Pfad. Liste Endpunkt, Query-Typ (read/write), wo Eingaben herkommen und welche Felder die Datenbank erreichen. Schließe Background-Jobs und Admin-Tools ein.

  2. Ersetze string-basiertes SQL durch Parameter. Suche nach Query-Konkatenation und tausche auf parametrisierte Queries oder einen Query-Builder. Mach das auch für „interne“ Endpunkte.

  3. Füge Allowlists für Identifier hinzu. Spaltennamen, Tabellennamen oder Sortierrichtungen kannst du nicht sicher parametrieren. Wenn Benutzereingaben ORDER BY, ausgewählte Spalten oder Joins steuern, mappe die Eingaben auf bekannte, sichere Identifier.

  4. Füge einige gezielte Tests hinzu. Sende Payloads, die Quoting brechen (einfaches Anführungszeichen), gängige Boolean-Tricks (OR 1=1) und unerwartete Typen. Behaupte sicheres Verhalten: keine zusätzlichen Zeilen, keine Datenleaks, keine SQL-Fehler, die sichtbar sind.

  5. Überprüfe Logs und Fehlerbehandlung erneut. Erzeuge Fehler absichtlich und stelle sicher, dass Responses kein rohes SQL, Stacktraces oder Treiberdetails enthalten. Halte detaillierte Fehler in Server-Logs und schwärze Werte, die sensible Daten enthalten könnten.

Sicherere ORM-Nutzung (und Fallstricke)

Ein ORM kann Injection blockieren, aber nur, wenn du auf seinen sicheren Wegen bleibst. Das bedeutet meist, das ORM das SQL bauen zu lassen und Benutzereingaben als Werte zu übergeben.

„Sichere“ Muster sehen aus wie "filter nach diesen Feldern", nicht wie "baue einen SQL-String".

// Example: safe parameter binding (generic)
const users = await db.user.findMany({
  where: {
    email: inputEmail,   // value is bound, not concatenated
    isActive: true
  },
  take: 25
});

// Example: query builder style with placeholders
const users2 = await knex('users')
  .where('email', '=', inputEmail)
  .andWhere('is_active', '=', true)
  .limit(25);

ORMs haben weiterhin Escapeways, und KI-generierter Code nutzt diese oft zu häufig. Sei vorsichtig bei:

  • Raw-Query-Helpern mit String-Interpolation
  • Methoden, die explizit als "unsafe" bezeichnet sind
  • Dem Einfügen von Benutzereingaben in Identifier (Spaltennamen, Tabellennamen)
  • APIs, die ein SQL-Fragment-String statt eines Wertes akzeptieren

Wenn du rohe SQL verwenden musst, nutze die Parameterfunktion des ORM, nicht Template-Strings. Für dynamische Spaltennamen verwende eine Whitelist.

Häufige Fehler beim Patchen von SQL-Injection

Schließe Admin-Panel-Lücken
Wir beheben fehlerhafte Authentifizierung und verschärfen Zugriffe, damit Admin-Tools nicht zur Schwachstelle werden.

Input-Validierung allein reicht nicht. Einige Zeichen zu blockieren oder Whitespace zu trimmen reduziert Geräusch, stoppt aber Injection nicht, wenn ein Wert weiterhin in einen SQL-String gelangt.

Hand-Escaping ist eine weitere Falle. Es fühlt sich sicherer an, Anführungszeichen zu ersetzen, aber Escape-Regeln unterscheiden sich je nach Datenbank und Randfälle sind leicht zu übersehen. Parametrisierte Queries sind sicherer und einfacher zu prüfen.

Prepared Statements können auch falsch verwendet werden. Ein häufiger Fehler ist, den Suchwert zu parametrieren, während man trotzdem SQL-Stücke wie ORDER BY konkatenieren lässt:

const sql = `SELECT * FROM users WHERE name ILIKE $1 ORDER BY ${sort} ${dir}`;
await db.query(sql, [`%${q}%`]);

Wenn sort oder dir aus der Request stammen, kann ein Angreifer ausbrechen. Behebe das mit einer strikten Allowlist für Identifier.

Logging ist ein unterschätztes Risiko. Volles SQL plus Benutzereingaben zu loggen kann E-Mails, Tokens oder andere Geheimnisse offenlegen, die in ein Suchfeld kopiert wurden. Halte Logs auf hohem Niveau und maskiere sensible Felder.

Realistisches Beispiel: Der verwundbare Admin-Search-Endpunkt

Ein Gründer verschickt schnell ein AI-generiertes Admin-Panel. Es hat ein einfaches Suchfeld: „Search users by email or name.“ Das Backend baut einen SQL-String aus dem, was der Admin tippt, und führt ihn so aus.

Das wirkt harmlos, weil es „nur Admin“ ist. Aber Admin-Endpunkte werden in der realen Welt exponiert: eine falsch konfigurierte Route, ein geleakter Cookie, ein schwaches Passwort oder ein internes Tool, das versehentlich öffentlich deployt wird.

Die Injection passiert, wenn die Such-Eingabe in eine Abfrage wie folgt eingesetzt wird:

SELECT id, email, role FROM users WHERE email LIKE '%{q}%' OR name LIKE '%{q}%'

Ein realistischer Payload schließt das Anführungszeichen und fügt eine wahre Bedingung hinzu, z. B. %' OR 1=1 --. Jetzt matcht die WHERE-Klausel immer, und die Antwort kann weit mehr Daten ausgeben als beabsichtigt.

In einer gepatchten Version verwendet dieselbe Suche Parameter. Wenn jemand %' OR 1=1 -- abschickt, wird das als reiner Text behandelt und verhält sich wie eine normale (meist erfolglose) Suche.

Kurze Checkliste vor dem Release

Verhindere Injection-Regressions
Stoppe das Kopieren unsicherer SQL-Muster, indem du sicherere Defaults im Projekt setzt.

Bevor du es als erledigt markierst, mach eine schnelle Durchsicht an den Stellen, an denen Injection noch versteckt sein kann.

  • Suche im Code nach SQL-Text in der Nähe von Request-Werten. Wenn du String-Konkatenation oder Template-Strings siehst, behandle das als unsicher, bis das Gegenteil bewiesen ist.
  • Überprüfe alle dynamischen Klauseln, nicht nur WHERE. ORDER BY, LIMIT/OFFSET und IN (...) sind häufige Überbleibsel.
  • Bestätige, dass jeder benutzerkontrollierte Wert als Parameter gebunden ist (einschließlich numerischer Eingaben wie IDs und Page-Sizes).
  • Sorge dafür, dass Fehler kein SQL-Text oder Stacktraces leaken.
  • Teste risikoreiche Routen (Login, Suche, Admin-Filter) mit ein paar einfachen Payloads und verifiziere, dass das Verhalten inert bleibt.

Nächste Schritte: Den sicheren Weg zum Standard machen

Der wirkliche Gewinn ist nicht, eine Abfrage zu patchen. Es geht darum, es schwer zu machen, dass Injection beim nächsten Mal wieder eingeführt wird, wenn ein KI-Tool einen "hilfreichen" CRUD-Endpunkt generiert.

Eine einfache Regel, die die meisten Regressionen verhindert: Kein rohes SQL, das aus Request-Strings gebaut wird. Wenn du dynamische Sortierung oder Feldauswahl brauchst, verwende strikte Allowlists.

Wenn du einen KI-generierten Codebestand geerbt hast und eine schnelle, strukturierte Überprüfung willst: FixMyMess (fixmymess.ai) konzentriert sich auf die Diagnose und Reparatur dieser Prototype-Probleme, inklusive unsicherer Query-Muster, Authentifizierungsfehlern und Sicherheits-Hardening, bevor sie in Produktion gelangen.