API-Sicherheit: JWT und OAuth, Validierung, Rate Limiting, Überwachung
Praktische Umsetzung der API-Sicherheit: JWT- und OAuth-Authentifizierung, Autorisierung, Verschlüsselung, Eingabenvalidierung, Rate Limiting sowie Protokollierung zur Erkennung von Angriffen.
Quellen: What Are the Best Practices for API Security? A Complete Guide to Securing Modern APIs
Jede API, die Sie bereitstellen, ist ein Eingang zu Ihrem System, und Angreifer prüfen diese Eingänge weitaus systematischer, als es die meisten Teams tun. Sicherheitsmaßnahmen, die erst nach dem Veröffentlichen hinzugefügt werden, hinterlassen oft Lücken: einen Pfad ohne Schutz, eine Abfrage, die aus Rohdaten erstellt wird, oder einen Anmeldepunkt, der gerne eine Million Versuche akzeptiert. Dieser Leitfaden führt Sie durch die wichtigsten Standards, die häufigsten Bedrohungen sowie sechs konkrete Verteidigungsschichten, die in Node.js mit Express implementiert werden können, damit Sie eine bestehende API audieren oder von Anfang an eine neue API mit integrierter Schutzfunktion erstellen können.
Betrachten Sie den folgenden Inhalt als Checkliste, zu der Sie während des gesamten Entwicklungszyklus zurückkehren sollten: vor der Veröffentlichung, nach einem Patch sowie immer dann, wenn sich Abhängigkeiten oder Pfade ändern. Regelmäßige Durchführung dieser Überprüfungen ermöglicht es, Schwachstellen bereits in einem frühen Stadium zu erkennen.
Warum API-Sicherheit besondere Aufmerksamkeit verdient
Moderne Produkte werden zunehmend aus APIs zusammengesetzt. Anstatt jede Funktion intern zu entwickeln, integrieren Teams Zahlungs-, Identitäts-, Kommunikations- und Datendienste über gut definierte Schnittstellen, und viele Unternehmen liefern heute Produkte, die auf APIs basieren. Eine Marktstudie schätzt die API-Wirtschaft für das Jahr 2026 auf etwa 20 Milliarden Dollar (Berichtszusammenfassung); unabhängig von der genauen Zahl ist die Abhängigkeit real und nimmt zu.
Diese Abhängigkeit wirkt sich in beide Richtungen aus. Eine API bietet legitimen Kunden sofort nutzbare, wiederverwendbare Funktionalitäten, bietet Angreifern aber gleichzeitig einen dokumentierten, maschinenfreundlichen Einstiegspunkt. Branchenumfragen weisen konsequent einen großen Anteil an Sicherheitsverletzungen auf APIs zurück; ein Bericht von Traceable aus dem Jahr 2023 schreibt 74 Prozent der Datenpannen API-Problemen zu.
APIs liegen häufig direkt vor den sensibelsten Daten, die ein Unternehmen besitzt: Identitätsplattformen mit persönlichen Daten, Finanzunterlagen sowie interne Arbeitsabläufe. Ein unbefugter Zugriff kann zu beschädigten Daten, missbräuchlich genutzten Diensten, finanziellen Verlusten, einem verlorenen Kundenvertrauen sowie Strafen nach dem Datenschutzgesetz führen. Da die Konsequenzen so gravierend sind, gehört die Sicherheit zusammen mit der Verfügbarkeit zu den Service-Level-Agreements und ist die Verantwortung jedes Produktteams – nicht nur einer speziellen Sicherheitseinheit. Ein sinnvoller Ausgangspunkt sind die Standards, auf die sich die Branche bereits geeinigt hat.
Standards, die man kennen sollte – von OWASP über OAuth bis JWT
Standards für API-Sicherheit sind formale Spezifikationen, Protokolle und Richtlinien, die im gesamten Softwareentwicklungslebenszyklus angewandt werden, damit der Schutz konsistent statt improvisiert erfolgt. Die gängigsten davon sind:
- OWASP API Security Top 10: Eine rangierte Liste der kritischsten API-Risiken, die vom Open Web Application Security Project gepflegt wird. Sie umfasst Probleme wie fehlerhafte Autorisierung auf Objektebene, Serverseitige Anfragenfälschung sowie unbeschränkten Ressourcenverbrauch und stellt den besten Ausgangspunkt für eine Bedrohungsbewertung dar.
- OAuth 2.0 und 2.1: Ein Delegierungsautorisierungsframework. Ein Client erhält einen Zugriffstoken mit definierten Berechtigungenskreisen und verwendet ihn, um im Namen eines Benutzers zu handeln, ohne jemals dessen Passwort zu verarbeiten; Erneuerungstoken ermöglichen es dem Client, neue Zugriffstoken zu erhalten, ohne den Benutzer erneut um Aktion bitten zu müssen.
- OpenID Connect (OIDC): Eine Identitätsschicht auf Basis von OAuth. Sie standardisiert einen Identitäts-Token sowie die Art und Weise, wie Clients diesen validieren, wodurch ein einmaliges Anmelden sowie das Abrufen von Profilen von Identitätsanbietern interoperabel werden.
Diese bieten eine solide Grundlage, sind jedoch keine ausführliche Liste.
Die Bedrohungen, gegen die Sie sich wehren müssen
Die am häufigsten in echten APIs auftretenden Schwachstellen sind:
- Defekte Authentifizierung: Schwache oder fehlende Identitätsprüfungen sowie mangelhafte Handhabung von Sitzungen ermöglichen es Angreifern, Cookies oder Tokens zu stehlen und sie erneut zu nutzen, um auf Ihre Dienste zuzugreifen.
- Defekte Autorisierung auf Objektebene (BOLA): Die API überprüft zwar, ob ein Benutzer eingeloggt ist, prüft jedoch nicht, ob ihm der Zugriff auf ein bestimmtes Objekt gestattet ist; dadurch kann das Ändern einer ID in der URL die Daten oder internen Abläufe anderer Personen preisgeben.
- SQL-Injection: Von einem Angreifer kontrollierte Eingaben werden in eine Abfrage eingefügt und von der Datenbank ausgeführt. Nahezu jeder Datenbankklient bietet ein Parametermechanismus, der Werte sicher übermittelt.
- Command-Injection: Unvertrauenswürdige Eingaben aus einer Anfrage erreichen das Systemshell oder den Befehlsprozessor, wodurch der Angreifer mit den Berechtigungen Ihres Serverprozesses beliebige Befehle ausführen kann.
Jedes dieser Probleme hat eine entsprechende Programmierpraxis. Der Rest dieses Leitfadens behandelt sie in sechs Ebenen, wobei ausschließlich JavaScript und Express verwendet werden.
1. Authentifizierung mit Passwörtern und JWT: Nachweis der Identität des Aufrufers
Jede geschützte API sollte vom Client vor der Ausführung irgendwelcher bedeutender Aktionen seine Identität nachweisen lassen – sei es über Benutzername und Passwort, eine API-Schlüssel oder ein signiertes Token. Authentifizierungsmethoden lassen sich im Allgemeinen in fünf Kategorien einteilen: Benutzername und Passwort, Mehrfaktorauthentifizierung, tokenbasierte Authentifizierung, zertifikatsbasierte Authentifizierung und Biometrie.
Eine häufige Quelle der Verwirrung ist, was ein JWT eigentlich ersetzt. Traditionelle Server-Sessions speicherten den Zustand auf dem Server und verließen sich auf Browser-Cookies, um eine Session-ID zu übertragen. Ein JWT beseitigt die Notwendigkeit, bei jeder Anfrage auf der Serverseite nachzuschauen, überprüft aber die Anmeldeinformationen nicht selbst – jemand muss den Passwort noch einmal prüfen, bevor der Token ausgestellt wird. Deshalb eignet sich ein hybrider Ablauf gut. Der Benutzer meldet sich mit E-Mail und Passwort an, der Server gibt bei Erfolg ein JWT aus, und jede nachfolgende Anfrage enthält nur noch den Token. Die Überprüfung der Anmeldeinformationen sowie die Authentifizierung pro Anfrage werden zu getrennten Aufgaben, und das Passwort muss nicht mehr mit jeder Anfrage übertragen werden.
In Express unterstützt die Passport-Bibliothek beide Ansätze durch plattformunabhängige Strategien. Die Einrichtung erfolgt in drei Schritten.
Schritt 1: Eine lokale Strategie sowie eine JWT-Strategie registrieren
Die lokale Strategie wird beim Anmelden einmal ausgeführt und ist dafür verantwortlich, den Benutzer anhand der E-Mail zu finden sowie das eingegebene Passwort mit dem gespeicherten Hash zu vergleichen. Die JWT-Strategie wird bei jeder geschützten Anfrage ausgeführt: Sie extrahiert das Token aus dem Authorization: Bearer-Header, überprüft die Signatur gegenüber JWT_SECRET und ermittelt den im Payload referenzierten Benutzer. Das Exportieren eines vorkonfigurierten authenticateJWT-Middlewares mit session: false macht den zustandslosen Ansatz explizit.
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const { Strategy: JwtStrategy, ExtractJwt } = require("passport-jwt");
// Local Strategy: Verify username and password during login.
passport.use(
new LocalStrategy(
{ usernameField: "email", passwordField: "password" },
async (email, password, done) => {
// Find the user and compare the hashed password.
// If valid, return the user.
}
)
);
// JWT Strategy: Verify the token on protected requests.
passport.use(
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
},
async (payload, done) => {
// Find the user referenced in the token.
}
)
);
// Middleware
const authenticateJWT = passport.authenticate("jwt", { session: false, });
module.exports = { passport, authenticateJWT, };
Die Überprüfungs-Callbacks werden hier als Kommentare hinterlassen, und genau dort findet die eigentliche Sicherheitsarbeit statt. Verwenden Sie einen langsamen, gesalzten Hashing-Algorithmus wie bcrypt oder Argon2 für den Passwortvergleich, und geben Sie immer denselben allgemeinen Fehler zurück, egal ob die E-Mail unbekannt ist oder das Passwort falsch, damit das Endpunkt nicht dazu genutzt werden kann, herauszufinden, welche Konten vorhanden sind.
Schritt 2: Authentifizierung beim Anmelden und Erstellung eines Tokens
Der Anmeldehandler ruft die lokale Strategie über einen benutzerdefinierten Callback auf. Fehler werden an Expresss Fehlerbehandlung weitergeleitet, fehlende Benutzer erzeugen einen 401-Status, und bei erfolgreicher Abgleichung wird ein Token erstellt, das mit der Benutzer-ID, der Rolle sowie einer Ablaufzeit aus JWT_EXPIRES_IN – standardmäßig zwei Stunden – signiert wird.
const jwt = require("jsonwebtoken");
const passport = require("passport");
const login = (req, res, next) => {
passport.authenticate("local", { session: false }, (err, user, info) => {
if (err) return next(err);
if (!user) return res.status(401).json({ message: info.message, });
// Issue a signed JWT after successful authentication.
const token = jwt.sign(
{ id: user.id, role: user.role,},
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || "2h",}
);
return res.status(200).json({ message: "Login successful.", token, user,});
})(req, res, next);
};
Zwei Aspekte verdienen besondere Aufmerksamkeit. Die kurze Gültigkeitsdauer begrenzt, wie lange ein gestohlener Token noch nützlich ist; wenn Sitzungen länger andauern müssen, sollten kurze Zugriffstoken mit einem Erneuerungsprozess kombiniert werden, wie er in unserer Refresh-Token-Strategie für Node.js-Authentifizierungssysteme beschrieben wird. Zudem gibt die Antwort das Feld user unverändert zurück. Wenn dieses Objekt eine rohe Datenbankzeile ist, kann es das Passworthash sowie interne Felder enthalten – genau das ist die zuvor beschriebene übermäßige Datenoffenlegung. Stattdessen sollte ein expliziter Teilmenge wie ID, E-Mail und Rolle zurückgegeben werden.
Schritt 3: Schutz von geschützten Routen
Mit dem exportierten Middleware bedeutet der Schutz einer Route, authenticateJWT vor dem Controller in der Routendefinition einzufügen. Anfragen ohne gültiges Token werden bereits vor dem Ausführen jeglicher Geschäftslogik abgelehnt.
const { Router } = require("express");
const authRouter = Router();
// Get auth middleware and sample prorected controller
const authController = require("../controllers/auth.controller");
const { authenticateJWT } = require("../middleware/authentication");
// Use JWT as a guard to protect certain routes
authRouter.get("/me", authenticateJWT, authController.me);
authRouter.patch("/password", authenticateJWT, authController.updatePassword);
Falls Sie schlankere Router bevorzugen, kann derselbe Schutz stattdessen in die eigene Middleware-Kette des Controllers integriert werden. In jedem Fall sollte der Schutz standardmäßig für einen Router aktiviert sein und öffentliche Routen bewusst ausgenommen werden, anstatt sich daran zu erinnern, den Schutz für jede Route einzeln hinzuzufügen.
2. Autorisierung und OAuth-Scope: Entscheidung darüber, was der Aufrufer tun darf
Die Authentifizierung beantwortet die Frage „Wer sind Sie?“, während die Autorisierung die Frage „Was dürfen Sie tun?“ beantwortet. Sie findet in der Regel unmittelbar nach der Authentifizierung statt und prüft jede Identität gegen Zugriffsregeln, bevor eine Anfrage genehmigt oder abgelehnt wird. Ohne sie kann jeder angemeldete Benutzer sensible Daten einsehen oder privilegierte Aktionen auslösen – genau so entstehen BOLA-Sicherheitslücken.
Drei Modelle decken die meisten Anforderungen ab:
- Rollenbasierte Zugriffskontrolle (RBAC) weist Berechtigungen Rollen zu und Rollen wiederum Benutzern. Eine Blogging-API kann Rollen wie Administrator, Editor und Betrachter haben. Sie eignet sich für stabile Arbeitsfunktionen und -gruppen, weshalb sie in Unternehmensanwendungen häufig vorkommt.
- Attributbasierte Zugriffskontrolle (ABAC) bewertet Attribute des Benutzers, der Ressource sowie der Anfragenumgebung (Abteilung, Sensitivität der Ressource, Tageszeit, Netzwerk) im Vergleich zu Richtlinien. Sie ist geeignet für APIs, deren Entscheidungen stark vom Kontext abhängen oder sich häufig ändern.
- Beziehungsbasierte Zugriffskontrolle (ReBAC) gewährt Zugriff auf der Grundlage der Beziehung zwischen einem Benutzer und einer bestimmten Ressource, wie zum Beispiel Eigentumsverhältnis oder Gruppenzugehörigkeit, wobei dies in der Regel durch das Durchlaufen eines Beziehungsgraphen überprüft wird. Sie eignet sich besonders gut für kollaborative Produkte wie Dokumentenfreigabesysteme oder soziale Plattformen.
Die eigene Erstellung einer Genehmigung ist eine hervorragende Möglichkeit, die Feinheiten zu verstehen, doch Produktionsysteme delegieren die Ausstellung und Überprüfung von Tokens oft an einen Identitätsanbieter. Wenn eine API bei einem Anbieter wie Microsoft Entra ID registriert ist und so konfiguriert ist, dass sie Träger-Token akzeptiert, überprüfen sensible Endpunkte vor der Ausführung die Berechtigungen und Rollen jedes Tokens. Ein ungültiges Token oder fehlende Berechtigungen führen zu einem 401 Unauthorized. In Express sieht der geschützte Pfad wie folgt aus:
app.get(
"/api/orders",
passport.authenticate("oauth-bearer", { session: false }),
(req, res) => {
res.json({ message: "Protected resource." });
}
);
Denken Sie daran, dass ein validierter Token nur grobe Berechtigungen festlegt. Prüfungen auf Objektebene, wie zum Beispiel „Gehört diese Bestellung zu diesem Benutzer?“, müssen weiterhin in Ihrem Handler oder in der Datenlage durchgeführt werden, denn kein Identitätsanbieter weiß, wem Zeile 4812 in Ihrer Datenbank gehört.
Was die Protokolle selbst angeht, sollten Sie auf Branchenstandards wie OAuth 2.0, OpenID Connect und SAML zurückgreifen. Sie können die entsprechenden Abläufe selbst umsetzen oder sie an Identitätsanbieter wie Ping Identity, Okta, Microsoft Entra ID, AWS oder IBM Security Verify delegieren.
3. Verschlüsselung: Schutz von Daten in Transit und im Ruhezustand
Die Verschlüsselung wandelt lesbares Datenmaterial in Verschlüsselungstext um, der ohne den richtigen Schlüssel nutzlos ist. TLS schützt die Daten während ihres Transports; die Verschlüsselung im Ruhezustand schützt sie dort, wo sie gespeichert sind, einschließlich der Datenbank. Sensible Systeme verwenden in der Regel beides, denn ohne Verschlüsselung können Daten wie finanzielle Zugangsdaten abgefangen oder aus einem kompromittierten Speicher entwendet werden.
Die wichtigsten Ansätze tauschen Geschwindigkeit gegen Schlüsselverwaltung ein:
- Symmetrische Verschlüsselung verwendet einen gemeinsamen Schlüssel. Sie ist sehr schnell und kann große Datenmengen gut verarbeiten, wodurch sie die richtige Wahl für die Verschlüsselung im Ruhezustand sowie von Payloads ist – doch beide Parteien müssen denselben geheimen Schlüssel sicher aufbewahren.
- Asymmetrische Verschlüsselung verwendet ein öffentliches und ein privates Schlüsselpaar, sodass kein gemeinsamer geheimer Schlüssel ausgetauscht werden muss. Sie ist erheblich langsamer und nur für kleine Datenmengen praktikabel.
Bei der symmetrischen Verschlüsselung unterstützt das eingebaute crypto-Modul von Node AES-256-GCM. Der untenstehende Handler serialisiert den Anfragekörper, erzeugt einen neuen 12-Byte-Initialisierungsvektor, verschlüsselt die Daten und gibt den IV, das GCM-Authentifizierungstag sowie den Verschlüsselungstext als Hexadezimalzeichenfolgen zurück.
const crypto = require("crypto");
const algorithm = "aes-256-gcm";
const key = Buffer.from(process.env.ENCRYPTION_KEY, "hex");
app.post("/api/orders", (req, res) => {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv(algorithm, key, iv);
const encrypted = Buffer.concat([
cipher.update(JSON.stringify(req.body), "utf8"),
cipher.final(),
]);
const payload = {
iv: iv.toString("hex"),
tag: cipher.getAuthTag().toString("hex"),
data: encrypted.toString("hex"),
};
// Store or transmit the encrypted payload
res.json(payload);
});
Mehrere Faktoren sorgen dafür, dass dies korrekt ist. Der Schlüssel muss genau 32 Bytes groß sein (64 Hexadezimalzeichen in ENCRYPTION_KEY) und sollte aus einem Secrets-Manager stammen und nicht aus dem Quellcode. Der IV muss für jede Verschlüsselung unter demselben Schlüssel einzigartig sein; die Wiederverwendung eines IVs mit GCM hat katastrophale Folgen, weshalb er pro Anfrage erzeugt wird. Der Authentifizierungstag ermöglicht es der entschlüsselnden Seite, Manipulationen zu erkennen, weshalb er zusammen mit dem Verschlüsseltext gespeichert und bei der Entschlüsselung überprüft werden muss. In einem echten Service würde man diesen Payload persistieren oder weiterleiten anstatt ihn, wie in der Demo, an den Aufrufer zurücksenden.
Assymmetrische Verschlüsselung ist ebenfalls im selben Modul verfügbar. Mit einer öffentlichen Schlüssel verschlüsselte Daten können nur mit dem entsprechenden privaten Schlüssel entschlüsselt werden:
const crypto = require("crypto");
const encrypted = crypto.publicEncrypt(
publicKey,
Buffer.from("Sensitive API data")
);
Weil RSA nur einen kleineren Payload als die Größe seiner Schlüssel verschlüsseln kann, eignet sich publicEncrypt für kurze Werte wie ein Geheimfeld oder einen symmetrischen Schlüssel, nicht für ganze Dokumente. Diese Einschränkung wird durch den hybriden Workflow umgangen: Es wird ein temporärer AES-Schlüssel erzeugt, der Payload mit diesem verschlüsselt, der AES-Schlüssel anschließend mit dem öffentlichen RSA-Schlüssel des Empfängers verschlüsselt und beides gesendet. Der Empfänger verwendet seinen privaten Schlüssel, um den AES-Schlüssel wiederherzustellen, und verschlüsselt danach den Payload. Die meisten APIs benötigen dies in ihrem Anwendungscode nie, da TLS bereits einen ähnlichen Austausch vornimmt, doch es lohnt sich, dieses Verfahren für Szenarien der Ende-zu-Ende-Verschlüsselung zu verstehen.
4. Eingabenvalidierung und -reinigung
Sobald Ihre API Client-Daten akzeptiert, können Sie nicht vorhersagen, was eintreffen wird. Falsch formatierte Datenkörper, SQL-Fragmente sowie Skript-Payloads sehen alle wie gewöhnliche Zeichenketten aus, bis etwas sie interpretiert. Zwei komplementäre Techniken begegnen diesem Problem. Validierung lehnt Eingaben ab, die Ihre strukturellen und semantischen Regeln verletzen. Sanitierung wandelt die akzeptierten Eingaben in eine sichere, normalisierte Form um, bevor sie Ihre Verarbeitungsfunktionen erreichen.
Erzwingen Sie zunächst den Inhaltstyp
Die günstigste Überprüfung ist die Formatierung der Anfrage selbst. Diese kleine Middleware-Fabrik verwendet req.is(), um den Content-Type zu überprüfen, und antwortet andernfalls mit 415 Unsupported Media Type. Sie kann global, pro Router oder pro Endpunkt eingesetzt werden.
const requireContentType = (type) => (req, res, next) => {
if (!req.is(type)) {
return res.status(415).json({ error: "Unsupported Media Type", });
}
next();
};
app.post("/api/users", requireContentType("application/json"),
(req, res) => {
res.json({ message: "User created." });
}
);
Überprüfen Sie die Struktur und den Sinn des Anfragekörpers
Sobald die Formatierung vorgegeben ist, prüft die nächste Schicht, ob die Anfrage korrekt strukturiert ist. Mit express-validator sollten die Regeln in einem eigenen Validierungsmodul aufbewahrt werden. Dieses Modul erfordert eine syntaktisch gültige E-Mail-Adresse, führt eine asynchrone, benutzerdefinierte Überprüfung durch, die Adressen, die bereits in der Datenbank vorhanden sind, ablehnt, und stellt außerdem eine Mindestlänge von acht Zeichen für das Passwort sicher.
const { body } = require("express-validator");
const { getUserEmail } = require("../db/queries");
const validateRegistration = [
body("email")
.isEmail()
.withMessage("Invalid email format")
.custom(async (value) => {
if (await getUserEmail(value)) {
throw new Error("Email is already in use");
}
return true;
}),
body("password")
.isLength({ min: 8 })
.withMessage("Password must be at least 8 characters long"),
];
module.exports = { validateRegistration }
Der Validierungsarray wird anschließend in die Middleware-Kette der Route eingefügt. Im Handler sammelt validationResult(req) alle Fehler, und die Route gibt anstelle des Weiterverarbeitens eine 400-Statusmeldung mit der vollständigen Fehlerliste zurück.
const { validationResult } = require("express-validator");
const { validateRegistration } = require("../validators/userValidator");
app.post("/api/register", validateRegistration, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
res.json({ message: "Registration successful." });
});
Sanitärisieren nach der Validierung
Weil Express die Middleware nacheinander verarbeitet, kann eine Sanitärisierungslogik direkt nach der Validierung folgen. Hier wird der Vorname gekürzt und HTML-kodiert, und die E-Mail-Adresse wird normalisiert.
const sanitizeRegistration = [
body("firstName").trim().escape(),
body("email").normalizeEmail(),
];
app.post(
"/api/register",
validateRegistration,
sanitizeRegistration,
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
res.json({ message: "Registration successful." });
}
);
Die Reihenfolge spielt hier auf subtile Weise eine Rolle. Die Einzigartigkeitsprüfung im Validator wird vor normalizeEmail() ausgeführt, sodass zwei unterschiedliche Schreibweisen derselben E-Mail-Adresse daran vorbeikommen und zu doppelten Konten führen können. Durch Normalisierung vor der Abfrage oder durch Durchsetzung der Einzigartigkeit am normalisierten Wert auf Datenbankebene wird diese Lücke geschlossen. Seien Sie außerdem vorsichtig mit escape(): Die HTML-Kodierung der Eingabe schützt die Templates, die den Wert ausgeben, verändert aber die gespeicherten Daten. Viele Teams bevorzugen es, rohe Werte zu speichern und stattdessen bei der Ausgabe zu kodieren. Wenn Sie Validierungsbibliotheken abwägen, behandelt unser Vergleich von Zod und express-validator die jeweiligen Vor- und Nachteile.
Verwenden Sie parametrisierte Abfragen für die Datenbank
Erstellen Sie niemals SQL-Abfragen durch Zusammenfügen von Benutzereingaben. Datenbankklienten wie pg sowie ORMs wie Prisma unterstützen parametrisierte Abfragen, bei denen der Abfragetext und die Werte getrennt übertragen werden, sodass die Datenbank die Eingaben stets als Daten und niemals als ausführbaren SQL-Code behandelt.
Mit pg erstellen Sie einmal einen Connection Pool und exportieren Sie ihn für Ihre Datenmodule:
const { Pool } = require("pg");
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
module.exports = pool;
Abfragen verwenden anschließend nummerierte Platzhalter ($1, $2), wobei die Werte als separates Array übergeben werden. Selbst wenn email einen Anführungszeichen folgend von DROP TABLE enthält, wird es als literale Zeichenkette gespeichert.
app.post("/api/users", async (req, res) => {
const { email, name } = req.body;
await pool.query(
"INSERT INTO users (email, name) VALUES ($1, $2)",
[email, name]
);
res.status(201).json({ message: "User created." });
});
Zusammen bilden diese Komponenten ein mehrschichtiges Pipeline-System. Express-validator ermöglicht es, Regeln als wiederverwendbare Einheiten zu isolieren, die als Middleware laufen und jeden Fehler melden, während die Parameterisierung sicherstellt, dass selbst Eingaben, die der Validierung entgehen, Ihre Abfragen nicht verfälschen können.
5. Rate Limiting und Throttling
Das Rate Limiting begrenzt, wie viele Anfragen ein Client innerhalb eines Zeitraums stellen darf. Es dämpft Angriffe durch Brute-Force-Methoden sowie Denial-of-Service-Angriffe und verhindert, dass ein besonders ressourcenintensiver Client alle anderen ausbremst.
Die Beschränkungen können in verschiedenen Dimensionen angewendet werden:
- Pro Client: Die Anfragen werden pro API-Schlüssel oder IP-Adresse gezählt. Wenn ein Client die Obergrenze erreicht, muss er warten, bis der Zeitraum neu beginnt, oder sich eine höhere Quotenmenge besorgen, meist in einem kostenpflichtigen Tarif.
- Nach Geografie oder Zeit: Die Einschränkungen variieren je nach Region oder Zeitraum – beispielsweise wird mehr Traffic aus den Regionen zugelassen, in denen Ihre Kunden tätig sind, während in den Regionen, aus denen verdächtiger Traffic stammt, strengere Beschränkungen gelten.
- Nach Serverkapazität: Einige Teile einer API werden an spezielle Infrastrukturen weitergeleitet, die eigene Einschränkungen haben, wie beispielsweise eine kleine Gruppe von Hintergrundprozessen für aufwändige Aufgaben.
Es existieren viele Algorithmen (fester Zeitraum, schräger Zeitraum, Token-Bucket-Algorithmus), und Sie müssen sie nicht selbst implementieren, um loszulegen. Das Middleware-Modul express-rate-limit zählt standardmäßig die Anfragen pro IP-Adresse. Das untenstehende Beispiel legt ein allgemeines Limit von 100 Anfragen pro 15 Minuten für alles unter /api fest sowie ein weitaus strengeres Limit von fünf Versuchen pro fünf Minuten für Anmeldungen, wobei für abgelehnte Anfragen eine benutzerdefinierte Nachricht angezeigt wird.
const rateLimit = require("express-rate-limit");
// Apply to all API routes
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
});
// Apply stricter limits to authentication endpoints
const loginLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 5,
message: "Too many login attempts. Please try again later.",
});
app.use("/api", apiLimiter);
app.post("/api/login", loginLimiter, (req, res) => {
res.json({ message: "Login successful." });
});
Öffentliche APIs geben in der Regel jedem Nutzer einen Schlüssel aus, und die Beschränkung über diesen Schlüssel ist fairer als die Beschränkung über die IP-Adresse, da viele Benutzer eine Adresse hinter einem Unternehmens-Proxy teilen können. Ein benutzerdefinierter keyGenerator liest den X-API-Key-Header und verwendet ihn als Identifikator für den Zähler.
const rateLimit = require("express-rate-limit");
const apiKeyLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 1000,
keyGenerator: (req) => req.get("X-API-Key"),
});
app.use("/api", apiKeyLimiter);
Wie beschrieben erzeugt jede Anfrage, die den Header weglässt, denselben undefined-Schlüssel und teilt sich einen einzigen Bucket. In der Praxis sollten Schlüssellose Anfragen bereits früher in der Verarbeitungskette abgelehnt werden oder man sollte auf die IP-Adresse zurückgreifen.
Limitierung von ressourcenintensiven Endpunkten
Durch die Limitierung wird bestimmt, wie schnell Anfragen akzeptiert werden, damit plötzliche Anstiege den Service nicht überlasten. Die folgende Konfiguration verwendet dasselbe Middleware mit sehr kurzen Zeitfenstern: maximal zehn Anfragen pro Sekunde über die API und nur eine Suchanfrage alle zwei Sekunden pro Client, da die Suche der ressourcenintensivste Endpunkt ist.
const rateLimit = require("express-rate-limit");
// Throttle all API requests
const apiThrottle = rateLimit({
windowMs: 1000, // 1 second
max: 10, // Allow up to 10 requests per second
});
// Apply a stricter throttle to resource-intensive endpoints
const searchThrottle = rateLimit({
windowMs: 2000, // 2 seconds
max: 1, // Allow 1 request every 2 seconds
message: "Please wait before sending another search request.",
});
app.use("/api", apiThrottle);
app.get("/api/search", searchThrottle, (req, res) => {
res.json({ results: [] });
});
Strikt genommen handelt es sich dabei immer noch um eine Rate-Limiting-Strategie mit kleinen Zeitfenstern: Überschüssige Anfragen werden mit einem 429-Statuscode abgelehnt, anstatt verzögert zu werden. Wenn Sie eine echte Throttling-Strategie wünschen, die die Clients vor der Ablehnung verlangsamt, bietet ein Zusatzpaket wie express-slow-down schrittweise Verzögerungen. Beachten Sie außerdem, dass der Standard-In-Memory-Speicher pro Prozess zählt; daher benötigen Sie hinter einem Load Balancer mit mehreren Instanzen einen gemeinsamen Speicher wie Redis, damit die Limits gewahrt bleiben. In neueren Versionen von express-rate-limit wird die max-Option auch als limit bezeichnet – prüfen Sie die Dokumentation der von Ihnen installierten Version. Für eine leichte Alternative in TypeScript finden Sie unseren Artikel über einen minimalen Rate-Limiter für Express.
6. Protokollierung, Überwachung und Fehlererkennung
Man kann nicht auf einen Angriff reagieren, den man nie sieht. Protokollierung erfasst Anfragen und Antworten zusammen mit deren Metadaten, Kontext, Zeitenangaben und Fehlercodes, damit man Probleme beheben, Audits durchführen und die tatsächliche Nutzung verstehen kann. Überwachung verfolgt die Aktivitäten in Echtzeit, überwacht Indikatoren wie Latenz, Fehlerraten und Durchsatz und macht Anomalien sichtbar, die auf Missbrauch, nicht erfüllte Service-Level-Ziele oder ausgenutzte Schwachstellen hindeuten könnten.
Die wichtigsten Ansätze samt ihren Vor- und Nachteilen:
- Anfragenprotokollierung mit Middleware wie Morgan erfasst jede eingehende HTTP-Anfrage kostengünstig, liefert aber keine Informationen zur Systemgesundheit.
- Anwendungsprotokollierung mit Winston, Pino oder Bunyan dokumentiert Geschäfts- und Sicherheitsereignisse wie Anmeldungen, Datenbankoperationen und Fehler. Größere Systeme benötigen eine strukturierte Ausgabe sowie zentrale Speicherung, um diese Informationen nutzbar zu machen.
- Metriken mit Prometheus und Grafana überwachen die Anfragenrate, Latenz, CPU, Speicher sowie Fehlerraten. Sie zeigen aggregierte Trends, aber nicht, was in einer einzelnen Anfrage geschah.
- Zentrale Logverwaltung mit dem ELK Stack, OpenSearch, Splunk oder CloudWatch aggregiert Logs aus verschiedenen Diensten – allerdings zu Lasten zusätzlicher Infrastruktur und operativer Aufwände.
- Überwachung der Anwendungsleistung mit Datadog, New Relic oder Dynatrace kombiniert Logs, Metriken, Spuren sowie Alarmfunktionen in einer Plattform – allerdings zu höheren Lizenzkosten und komplexerer Plattformstruktur.
Jeder dieser Komponenten kann in Express eingebunden werden. Im Folgenden werden vier gängige Bausteine vorgestellt.
Anfragen mit Morgan protokollieren
Durch die Registrierung von Morgan im combined-Format wird für jede Anfrage eine im Stil von Apache formulierte Zeile geschrieben, die Methode, Pfad, Status, Antwortgröße sowie den User Agent enthält.
const express = require("express");
const morgan = require("morgan");
const app = express();
// Log every incoming request
app.use(morgan("combined"));
app.get("/api/users", (req, res) => {
res.json({ message: "Users retrieved successfully." });
});
Gestrukturiertes Anwendungsprotokollieren mit Winston
Winston speichert Ereignisse als gestrukturierte Objekte. Das Protokollieren der Benutzer- und Bestellnummern beim Erstellen einer Bestellung erzeugt eine Auditspur, die Sie später durchsuchen können.
const winston = require("winston");
const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
],
});
app.post("/api/orders", (req, res) => {
logger.info("Order created", {
userId: req.user.id,
orderId: req.body.id,
});
res.status(201).json({ message: "Order created." });
});
Achten Sie darauf, was in die Logs aufgenommen wird. Benutzer IDs sind in Ordnung; Passwörter, Tokens, vollständige Kartennummern sowie ganze Anfragekörper hingegen nicht – und Logs sind ein häufiger Ort, an dem sensible Daten durchsickern können.
Zentralisiertes Fehlerprotokollieren
Ein Express-Fehlerbehandlungs-Middleware, das an seinen vier Argumenten zu erkennen ist, fängt Fehler von jeder Route ab. Dieses Middleware protokolliert die Fehlermeldung zusammen mit dem Pfad und der Methode und gibt einen generischen 500-Status zurück, sodass Stack-Traces und interne Details niemals beim Client ankommen.
app.use((err, req, res, next) => {
/* Logger is built as an independent module or class */
logger.error(err.message, {
path: req.originalUrl,
method: req.method,
});
res.status(500).json({
error: "Internal Server Error",
});
});
Exponierung von Metriken für Prometheus
Die prom-client-Bibliothek sammelt die Standardmetriken des Node.js-Prozesses und stellt sie über einen /metrics-Endpunkt zur Verfügung, damit Prometheus sie abrufen kann.
const client = require("prom-client");
client.collectDefaultMetrics();
app.get("/metrics", async (req, res) => {
res.set("Content-Type", client.register.contentType);
res.end(await client.register.metrics());
});
Dieser Endpunkt gibt interne Details zu Ihrem Service preis, daher sollten Sie ihn auf Ihr Überwachungsnetzwerk beschränken oder hinter Authentifizierung schützen, anstatt ihn öffentlich zugänglich zu lassen.
Framework mit vorgegebenen Konzepten helfen dabei. NestJS kommt mit einem integrierten Logger sowie einer Struktur, die es erleichtert, für jeden Endpunkt Logging- und Metrikenfunktionen hinzuzufügen, während man bei einem framework ohne vorgegebene Konzepte wie Express das Logging gezielt hinzufügen muss – in der Regel als Middleware, die vor dem Versand der Antwort ausgeführt wird. Die Erkennung von Vorfällen basiert anschließend auf diesen Protokollen: Warnungen für verdächtige Muster, wie einen Anstieg an 401-Antworten oder Ablehnungen aufgrund von Rate-Limiting, werden an den Benachrichtigungskanal geleitet, den Ihr Team tatsächlich beobachtet.
Sicherheit als kontinuierlicher Prozess
Kein einzelner Artikel deckt alles ab, aber vor jeder Veröffentlichung können Sie sicherstellen, dass Ihre API diesen Grundstandard erfüllt:
- Jeder Endpunkt wird ausschließlich über HTTPS bereitgestellt.
- Es wird OAuth oder ein äquivalenter Token-Fluss verwendet.
- Ausgestellte JWTs haben eine Gültigkeitsdauer.
- Limitierungen schützen alle Routen, wobei bei der Anmeldung strengere Regeln gelten.
- Jeder Eingabewert wird vor der Verwendung überprüft.
- Abfragen wurden auf Injektionsversuche getestet.
- Zugriffsregeln wurden getestet, einschließlich objektspezifischer Überprüfungen.
- Schlüssel und Geheimnisse befinden sich außerhalb des Codebases.
- Sicherheitsrelevante Ereignisse werden protokolliert.
- Dashboards und Benachrichtigungen sind konfiguriert.
- Antworten enthalten Sicherheits-Header.
- Kunden sehen niemals Stack-Traces oder detaillierte interne Fehlermeldungen.
- Abhängigkeiten sind aktuell und wurden überprüft.
Bei den Beispielen wurde hier Express verwendet, doch die gleichen Prinzipien gelten auch für andere Backend-Frameworks, von denen die meisten entweder diese Tools direkt integrieren oder native Äquivalente bieten. NestJS beispielsweise kümmert sich um die Anfragevalidierung über Data Transfer Objects. Die Dokumentation Ihres Frameworks zeigt die für dieses Framework typische Umsetzung jeder Schicht.
Die gleichen Prinzipien bilden außerdem die Grundlage für cloud-native Deployment-Lösungen auf Plattformen wie Azure, Google Cloud und AWS, die eigene Gateways, Identitätsdienste sowie verwaltete Rate-Limiting-Funktionen hinzufügen. Cloud-spezifische Vorgehensweisen verdienen eine eigene Behandlung, doch die oben beschriebenen Schichten bringen eine API bereits weit genug voran, um eine Sicherheitsprüfung zu bestehen.
Kurze Sicherheitsprüfung einer Express-API
- Trennen Sie die Überprüfung der Zugangsdaten von der Authentifizierung pro Anfrage: Überprüfen Sie das Passwort einmal und verlassen Sie sich anschließend auf kurz gültige, signierte Token.
- Authentifizierung ist nicht Autorisierung. Überprüfen Sie Scope und Rollen sowie weiterhin das Eigentumsrecht an jedem Objekt, das von einer Anfrage berührt wird.
- Verwenden Sie AES-GCM mit einem eindeutigen IV pro Operation für ruhendes Datenmaterial; reservieren Sie asymmetrische Verschlüsselung für kleine Werte und Schlüsselaustausch.
- Fügen Sie Kontrollen des Content-Typs, Validierungen, Sanitierungen sowie parametrisierte Abfragen hinzu und berücksichtigen Sie die Reihenfolge ihrer Ausführung.
- Setzen Sie Limits für alle Abläufe, wenden Sie strengere Limits auf Anmeldungen und ressourcenintensive Endpunkte an, und verwenden Sie einen gemeinsamen Speicher, sobald Sie mehr als eine Instanz betreiben.
- Protokollieren Sie sicherheitsrelevante Ereignisse, ohne Geheimnisse mit aufzunehmen, und wandeln Sie diese Protokolle in Warnungen um, auf die jemand reagieren kann.
Weitere Literatur
- Wenn JWT-Authentifizierung staatlich wird: Ein Fall für Server-Seitige Sessions in Node: Erfahren Sie, wie Ablösungs-Blocklisten und Refresh-Speicher die JWT-Authentifizierung anfällig machen, wie Express-Sessions mit Postgres diese vereinfachen und wo JWTs weiterhin geeignet sind.
- Erstellung einer für die Produktion geeigneten JWT-Authentifizierung in Node.js-APIs: Wie man Passwörter hasht, kurzlebige JWTs ausstellt, Refresh-Tokens hinzufügt sowie Ablösungsfunktionen implementiert, Autorisierung trennt, Angriffe durch Brute Force abwehrt und überprüft, ob die Authentifizierung korrekt fehlschlägt.