Back

Sichere Verarbeitung von Benutzereingaben in Node.js

Sichere Verarbeitung von Benutzereingaben in Node.js

Jede Node.js-API empfängt Daten, denen sie nicht vollständig vertrauen kann. JSON-Bodies, Query-Strings, Formularfelder und Datei-Uploads kommen alle von außen, und jeder von ihnen kann schädliche Payloads enthalten. SQL-Injection, NoSQL-Injection, Command-Injection, Prototype-Pollution – diese Angriffe haben einen gemeinsamen Einstiegspunkt: unvalidierte Benutzereingaben, die die Business-Logik, eine Datenbank oder einen Shell-Befehl erreichen. Dieser Artikel führt durch die praktischen Patterns, die diese Angriffe verhindern.

Wichtigste Erkenntnisse

  • Validieren Sie eingehende Daten immer gegen ein striktes Schema, bevor sie Ihre Anwendungslogik erreichen – definieren Sie, was erlaubt ist, und lehnen Sie alles andere ab.
  • Verwenden Sie parametrisierte Queries für SQL und erzwingen Sie strikte Datentypen für NoSQL, um Injection-Angriffe zu verhindern.
  • Vermeiden Sie child_process.exec() für Systembefehle; verwenden Sie stattdessen execFile() oder spawn() mit expliziten Argument-Arrays.
  • Spreaden Sie niemals rohe Request-Bodies auf Models; wählen Sie nur die Felder aus, die Sie erwarten, um Mass-Assignment und Prototype-Pollution zu verhindern.
  • Setzen Sie Body-Größenlimits und validieren Sie Datei-Uploads auf Parser-Ebene, um sich gegen Denial-of-Service-Angriffe zu schützen.

Erst validieren, dann verarbeiten

Die wirksamste Regel bei der sicheren Request-Verarbeitung in Node.js-APIs lautet: Lassen Sie niemals rohe Request-Daten Ihre Anwendungslogik berühren. Validieren Sie Form und Typ jedes eingehenden Werts, bevor Sie ihn verwenden.

Schema-basierte Validierung ist der sauberste Ansatz. Bibliotheken wie Zod und Joi ermöglichen es Ihnen, genau zu deklarieren, wie ein Request aussehen sollte, und alles abzulehnen, was nicht passt.

import { z } from "zod"

const RegisterSchema = z.object({
  username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
  email: z.string().email(),
  password: z.string().min(10),
})

app.post("/register", (req, res) => {
  const result = RegisterSchema.safeParse(req.body)
  if (!result.success) {
    return res.status(400).json({ errors: result.error.flatten() })
  }
  // result.data ist jetzt sicher zu verwenden
})

Dies ist ein Allowlist-Ansatz: Sie definieren, was erlaubt ist, und alles andere wird abgelehnt. Das ist weitaus zuverlässiger als der Versuch, gefährliche Zeichen auf eine Denylist zu setzen. Dasselbe Prinzip wird im OWASP Input Validation Cheat Sheet betont.


Injection-Angriffe verhindern

SQL- und NoSQL-Injection

Konkatenieren Sie niemals Benutzereingaben in Queries. Verwenden Sie parametrisierte Statements für SQL:

// ❌ Verwundbar
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`

// ✅ Sicher
const result = await db.query("SELECT * FROM users WHERE email = $1", [email])

NoSQL-Datenbanken haben ihr eigenes Injection-Risiko. MongoDB-Operatoren wie $gt können über JSON-Bodies injiziert werden, wenn Sie Objekte direkt übergeben:

// ❌ Verwundbar — req.body.username könnte { $gt: "" } sein
User.find({ username: req.body.username })

// ✅ Sicher — erzwingen Sie zuerst den erwarteten Typ
User.find({ username: String(req.body.username) })

In der Praxis sollte die Schema-Validierung sicherstellen, dass das Feld ein String ist (z. B. mit z.string()), bevor es die Query-Ebene erreicht.

Command-Injection

Wenn Ihre API Systembefehle aufruft, vermeiden Sie child_process.exec() vollständig – es übergibt Argumente durch eine Shell und ist trivial ausnutzbar. Verwenden Sie stattdessen execFile() oder spawn() mit expliziten Argument-Arrays:

import { execFile } from "child_process"

// ✅ Argumente werden direkt übergeben, nicht von einer Shell interpretiert
execFile("convert", [userSuppliedFilename, outputPath], (err, stdout) => {
  // Ergebnis verarbeiten
})

Validieren Sie dennoch die Argument-Werte und setzen Sie sie auf eine Allowlist. Wenn die Operation hochriskant ist, führen Sie sie in einem isolierten Container aus.


Mass-Assignment und Prototype-Pollution vermeiden

Mass-Assignment tritt auf, wenn Sie einen Request-Body direkt auf ein Model spreaden:

// ❌ Ein Benutzer könnte { role: "admin" } senden
const user = await User.create({ ...req.body })

// ✅ Wählen Sie nur die Felder aus, die Sie erwarten
const user = await User.create({
  username: result.data.username,
  email: result.data.email,
})

Prototype-Pollution ist subtiler. Das Mergen nicht vertrauenswürdiger Objekte kann einem Angreifer ermöglichen, Properties wie __proto__ oder constructor.prototype zu injizieren, die jedes Objekt in Ihrem Prozess beeinflussen. Vermeiden Sie Deep-Merge-Utilities auf rohen Request-Daten. Wenn Sie mergen müssen, validieren Sie zuerst die Eingabe und erstellen Sie Zwischenobjekte mit Object.create(null), damit sie nicht von der Standard-Prototype-Chain erben. Das OWASP Prototype Pollution Prevention Cheat Sheet bietet weitere Anleitungen.


Request-Größe und -Struktur begrenzen

Große oder tief verschachtelte Payloads können Denial-of-Service-Probleme verursachen. Setzen Sie Body-Größenlimits auf Parser-Ebene:

app.use(express.json({ limit: "50kb" }))

Verwenden Sie für Datei-Uploads multer mit expliziten fileSize- und files-Limits und validieren Sie MIME-Typen – vertrauen Sie niemals allein der Dateiendung.


Fazit

Die sichere Verarbeitung von Benutzereingaben in Node.js läuft auf ein Prinzip hinaus: Definieren Sie, wie valide Eingaben aussehen, erzwingen Sie dies an der Grenze und lassen Sie nichts anderes durch. Schema-Validierung, parametrisierte Queries, explizite Argument-Übergabe und strikte Feldauswahl decken die Mehrheit der realen Angriffsflächen ab. Wenden Sie diese Patterns konsequent auf der Request-Ebene an, bevor Daten irgendeine nachgelagerte Logik erreichen, und die meisten Injection-basierten Schwachstellen haben einfach keine Angriffsfläche mehr.

FAQs

Schema-Validierung ist eine starke erste Verteidigungslinie, weil sie fehlerhafte Eingaben ablehnt, bevor sie Ihre Logik erreichen. Sie sollte jedoch mit parametrisierten Queries und sicherer Befehlsausführung kombiniert werden. Validierung stellt sicher, dass Daten die richtige Form und den richtigen Typ haben, aber der nachgelagerte Code muss diese Daten ebenfalls sicher verarbeiten, um Grenzfälle abzudecken, die die Validierung allein möglicherweise übersieht.

Beide Bibliotheken sind ausgereift und weit verbreitet. Zod bietet erstklassige TypeScript-Unterstützung mit starker Type-Inference, was es zur natürlichen Wahl für TypeScript-Projekte macht. Joi hat eine längere Erfolgsbilanz im Express-Ökosystem und bietet eine reichhaltige, verkettbare API. Wählen Sie basierend auf den Sprachpräferenzen Ihres Projekts und vorhandenen Abhängigkeiten.

Vermeiden Sie es, Deep-Merge-Funktionen auf rohen Request-Daten auszuführen. Validieren Sie Eingaben zuerst mit einem strikten Schema, sodass nur bekannte Felder durchkommen. Wenn Mergen notwendig ist, erstellen Sie Zwischenobjekte mit Object.create(null), um Objekte ohne Prototype-Chain zu erzeugen. Bibliotheken wie lodash.merge hatten in der Vergangenheit Prototype-Pollution-Schwachstellen, halten Sie daher Abhängigkeiten immer aktuell.

Es gibt keine universelle Antwort, aber 100 KB bis 1 MB decken die meisten JSON-API-Anwendungsfälle ab. Beginnen Sie mit einem konservativen Limit wie 100 KB und erhöhen Sie es nur, wenn legitime Requests mehr benötigen. Konfigurieren Sie für Datei-Uploads separate Limits über Ihren Multipart-Parser. Das Ziel ist es, ungewöhnlich große Payloads frühzeitig abzulehnen, bevor sie Speicher oder Verarbeitungszeit verbrauchen.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay