Startseite / Artikel / Absicherung von Express-APIs: Authentifizierung, Validierung, Rate Limits und Überwachung.

Absicherung von Express-APIs: Authentifizierung, Validierung, Rate Limits und Überwachung.

Ein praktischer Überblick über die Sicherheit von Express-APIs: Authentifizierung mit Passport und JWT, Autorisierungsmodelle, Verschlüsselung mit AES-GCM, Validierung, Rate Limiting und Logging.

5092 Wörter

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 Schutzmechanismus, eine Abfrage, die aus Rohdaten erstellt wird, oder einen Anmeldepunkt, der bereitwillig eine Million Versuche zulässt. 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 auditen oder von Anfang an eine neue 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 einer 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 APIs einer speziellen Sicherheitsaufmerksamkeit bedürfen

Moderne Produkte werden zunehmend aus APIs zusammengesetzt. Anstatt jede Funktion intern zu entwickeln, integrieren Teams Zahlungs-, Identitäts-, Nachrichten- und Datendienste über gut definierte Schnittstellen – viele Unternehmen liefern heute Produkte, die auf APIs basieren. Eine Marktstudie schätzt die API-Wirtschaft im Jahr 2026 auf etwa 20 Milliarden Dollar (Zusammenfassung des Berichts); unabhängig von der genauen Zahl ist die Abhängigkeit real und wächst weiter.

Diese Abhängigkeit wirkt sich in beide Richtungen aus. Eine API bietet legitimen Kunden sofort nutzbare, wiederverwendbare Funktionalitäten – gleichzeitig bietet sie Angreifern einen dokumentierten, maschinenfreundlichen Einstiegspunkt. Branchenumfragen weisen konsequent einen großen Anteil an Sicherheitsverletzungen auf APIs zurück; Traceables Bericht von 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 Informationen, Finanzunterlagen sowie interne Arbeitsabläufe. Unbefugter Zugriff kann zu beschädigten Daten, missbräuchlich genutzten Diensten, finanziellen Verlusten, zerstörtem 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 denen sich die Branche bereits geeinigt hat.

Standards und Frameworks, die man kennen sollte

API-Sicherheitsstandards sind formale Spezifikationen, Protokolle und Richtlinien, die im gesamten Softwareentwicklungslebenszyklus angewandt werden, damit der Schutz konsistent statt improvisiert erfolgt. Die am häufigsten vorkommenden 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 verarbeiten zu müssen; Erneuerungstoken ermöglichen es dem Client, neue Zugriffstokens 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 ID-Token sowie die Art und Weise, wie Clients diesen validieren, wodurch ein einmaliges Anmelden sowie das Abrufen von Profilen bei Identitätsanbietern interoperabel werden.
  • JSON Web Tokens (JWTs): ein kompakter, signierter Token-Format. Im Gegensatz zu einem unklaren Session-Cookie enthält ein JWT seine Angaben (Benutzer-ID, Rolle, Ablaufdatum) direkt im Token selbst, und der Server überprüft die Signatur anstelle einer Sessionabfrage. Beachten Sie, dass ein standardmäßig signierter JWT kodiert und nicht verschlüsselt ist, sodass jeder, der ihn besitzt, seine Inhalte lesen kann.
  • Transport Layer Security (TLS): das kryptografische Protokoll hinter HTTPS. Es gewährleistet die Verschlüsselung während der Übertragung, die Authentifizierung der Kommunikationsparteien sowie Integritätsprüfungen, die Manipulationen erkennen.
  • Zero Trust: eine architektonische Haltung, bei der jeder Benutzer und jede Dienstleistung als unzuverlässig angesehen wird, bis ihre Zuverlässigkeit nachgewiesen ist. Sie basiert auf expliziter Überprüfung, dem Prinzip der geringsten Berechtigungen sowie der Annahme, dass bereits irgendwo ein Sicherheitsvorfall eingetreten ist.
  • NIST Cybersecurity Framework: Leitlinien, die sich besonders gut auf Cloud- und Microservice-Architekturen anwenden lassen, wobei der Schwerpunkt auf Authentifizierung, Autorisierung und Datenschutz liegt.
  • Financial-grade API Profiles (FAPI): Erhöht gesicherte OAuth- und OIDC-Profile für hochriskante Bereiche wie Banking, Fintech und regulierte Daten. Sie erfordern eine starke Client-Authentifizierung, ein strengeres Handling von Anfragen und Antworten sowie senderbezogene Token, die bei Diebstahl nutzlos sind – dies verhindert Betrug und verbessert die Interoperabilität.
  • 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:

    1. 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.
    2. Defekte Autorisierung auf Objektebene (BOLA): Die API überprüft zwar, ob ein Benutzer angemeldet 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.
    3. 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.
    4. Befehls-Injection: Unzuverlässige Eingaben aus einer Anfrage erreichen das Systemshell oder den Befehlsprozessor, wodurch der Angreifer mit den Berechtigungen Ihres Serverprozesses beliebige Befehle ausführen kann.
  • Cross-Site-Scripting (XSS): Ein eingeschleuster Skript wird im Browser des Opfers im Kontext Ihrer Anwendung ausgeführt, sodass der Angreifer wie dieser Benutzer handeln und Daten lesen kann, die normalerweise durch die Same-Origin-Policy geschützt wären.
  • Sicherheitsfehlkonfiguration: Durchgesickerte API-Schlüssel, offengelegte Umgebungsvariablen sowie zu permissive Standardeinstellungen. Compromittierte Anmeldedaten können genutzt werden, um Ihre API aufzurufen oder Rechnungen für bezahlte Drittanbieterdienste zu verursachen.
  • Übermäßige oder sensible Datenoffenlegung: Ein Handler gibt das gesamte Datenbankobjekt anstelle der vom Client benötigten Felder zurück. Auch wenn die Benutzeroberfläche diese Daten nie anzeigt, befinden sie sich weiterhin in Caches, lokalen Speichern sowie im Netzwerk-Tab des Browsers.
  • Dienstverweigerung (einschließlich verteilter Angriffe): Eine Flut an Anfragen erschöpft die Serverressourcen oder bringt die API vollständig offline.
  • Drittanbieter-Abhängigkeiten: Ihre API ist außerdem ein Client anderer APIs und Pakete. Jedes davon erweitert Ihr Angriffsprofil, und ein Sicherheitsvorfall oder Ausfall dort wird zu Ihrem Problem.
  • 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: 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 fallen im Allgemeinen in fünf Kategorien: 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 Zugangsdaten nicht selbst – jemand muss dennoch vor Ausstellung des Tokens das Passwort überprüfen. Deshalb eignet sich ein hybrider Ablauf gut: Der Benutzer melden sich mit E-Mail und Passwort an, der Server gibt bei Erfolg ein JWT aus, und jede nachfolgende Anfrage enthält nur noch das Token. Die Überprüfung der Zugangsdaten 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. Durch das Exportieren eines vorkonfigurierten authenticateJWT-Middlewares mit session: false wird die stateless Ausrichtung explizit gemacht.

    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 sind 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 – unabhängig davon, ob die E-Mail unbekannt ist oder das Passwort falsch ist – damit der Endpunkt nicht dazu genutzt werden kann, herauszufinden, welche Konten existieren.

    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, während ein erfolgreicher Abgleich ein mit der Benutzer-ID und -Rolle signiertes Token sowie eine Ablaufzeit liefert, die aus JWT_EXPIRES_IN entnommen wird – falls nicht vorhanden, werden zwei Stunden verwendet.

    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 bestimmt, 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 user-Objekt unverändert zurück. Wenn es sich dabei um einen Rohdatensatz aus der Datenbank handelt, kann dieser das Passworthash sowie interne Felder enthalten – genau das ist die zuvor beschriebene übermäßige Datenoffenlegung. Stattdessen sollte ein expliziter Untermenge 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 Schutzmechanismus stattdessen in die eigene Middleware-Kette des Controllers eingebunden werden. In jedem Fall sollte der Schutz standardmäßig für Router aktiviert sein und öffentliche Routen bewusst ausgenommen werden, anstatt sich daran zu erinnern, den Schutzmechanismus für jede Route einzeln hinzuzufügen.

    2. Autorisierung: 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 wird in der Regel unmittelbar nach der Authentifizierung ausgeführt 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 beispielsweise Admin-, Editor- und Viewer-Rollen haben. Sie eignet sich für stabile Aufgabenfunktionen 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) anhand von Richtlinien. Sie ist geeignet für APIs, deren Entscheidungen stark vom Kontext abhängen oder sich häufig ändern.
    • Beziehungsbasierte Zugriffskontrolle (ReBAC) gewährt Zugang auf der Grundlage der Beziehung zwischen einem Benutzer und einer bestimmten Ressource, wie zum Beispiel Eigentumsverhältnissen 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 in der Regel an einen Identitätsanbieter. Wenn eine API bei einem Anbieter wie Microsoft Entra ID registriert ist und so konfiguriert wurde, dass sie Träger-Token akzeptiert, überprüfen sensible Endpunkte vor der Ausführung die Scopes 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 überprüftes Token nur grobe Berechtigungen festlegt. Objektbezogene Überprüfungen, 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.

    Für die Protokolle selbst 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 bei der Speicherung

    Durch Verschlüsselung werden lesbare Daten in unsichtbaren Schlüsseltext umgewandelt, der ohne den richtigen Schlüssel nutzlos ist. TLS schützt Daten während ihres Transports; die Verschlüsselung bei der Speicherung schützt sie dort, wo sie aufbewahrt werden, einschließlich der Datenbank. Sensible Systeme verwenden in der Regel beides, denn ohne Verschlüsselung können Daten wie Finanzdaten abgefangen oder aus einem kompromittierten Speicher entwendet werden.

    Die wichtigsten Ansätze bringen Geschwindigkeit und Schlüsselverwaltung miteinander in Einklang:

    • 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 ruhender Daten sowie von Payloads ist – allerdings müssen beide Parteien den gleichen geheimen Schlüssel sicher aufbewahren.
    • Asymmetrische Verschlüsselung nutzt ein Paar aus öffentlichem und privatem Schlüssel, sodass kein gemeinsamer geheimer Schlüssel ausgetauscht werden muss. Sie ist erheblich langsamer und nur für kleine Datenmengen praktikabel.
    • Hybride Verschlüsselung kombiniert beides: Die asymmetrische Kryptographie schützt einen symmetrischen Schlüssel, und dieser wiederum schützt die eigentlichen Datenmengen. So ergeben sich die Vorteile des Schlüsselaustauschs der ersten Methode zusammen mit der Geschwindigkeit der zweiten.

    Für symmetrische 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 Hexadezimalzeichenketten 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 lang 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 derselben Schlüssel einzigartig sein; die Wiederverwendung eines IVs mit GCM hat katastrophale Folgen, weshalb er pro Anfrage generiert wird. Der Authentifizierungstag ermöglicht es der Entschlüsselungsseite, 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 einem ö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 generiert, der Payload mit diesem verschlüsselt, anschließend wird der AES-Schlüssel 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 wiederzuerlangen, 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 akzeptierte Eingaben in eine sichere, normalisierte Form um, bevor sie Ihre Verarbeitungsfunktionen erreichen.

    Zuerst den Inhaltstyp durchsetzen

    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 eingebunden 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." });
      }
    );
    

    Form und Bedeutung des Datenkörpers validieren

    Sobald das Format vorgegeben ist, prüft die nächste Schicht, ob der Anfrage eine korrekte Struktur vorliegt. 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 Prüfung durch, die Adressen zurückweist, die bereits in der Datenbank vorhanden sind, 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 }
    

    Anschließend wird der Validierungsarray in die Middleware-Kette der Route eingefügt. Im Handler sammelt validationResult(req) alle Fehler, und die Route gibt stattdessen eine 400-Statusmeldung mit der vollständigen Fehlerliste zurück, anstatt weiterzumachen.

    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." });
    });
    

    Sanitisieren nach der Validierung

    Weil Express die Middleware nacheinander verarbeitet, kann eine Sanitisierungs-Kette direkt nach der Validierung folgen. Hier wird der Vorname gekürzt und HTML-kodiert, während die E-Mail-Adresse normalisiert wird.

    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 Eindeutigkeitsprüfung im Validator wird vor normalizeEmail() ausgeführt, wodurch zwei unterschiedliche Schreibweisen derselben E-Mail-Adresse daran vorbeikommen und zu duplizierten Konten führen können. Durch Normalisierung vor der Abfrage oder durch Durchsetzung der Eindeutigkeit 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

    Bauen Sie niemals SQL durch Zusammenfügen von Benutzereingaben. Datenbankklienten wie pg sowie ORMs wie Prisma unterstützen parametrisierte Abfragen, bei denen der Abfragetext und die Werte getrennt übermittelt werden, sodass die Datenbank den Eingang stets als Daten und niemals als ausführbaren SQL-Code behandelt.

    Mit pg erstellen Sie einmal einen Connection Pool und exportieren ihn für Ihre Datenmodule:

    const { Pool } = require("pg");
    
    const pool = new Pool({
      connectionString: process.env.DATABASE_URL,
    });
    
    module.exports = pool;
    

    Die 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 reiner Zeichenstring 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 Elemente ein schichtweises Pipeline-System. Express-validator ermöglicht es Ihnen, Regeln als wiederverwendbare Einheiten zu isolieren, die als Middleware laufen und jeden Fehler melden, während die Parametrisierung sicherstellt, dass selbst Eingaben, die der Validierung entgehen, Ihre Abfragen nicht umschreiben können.

    5. Rate Limiting und Throttling

    Das Rate Limiting begrenzt, wie viele Anfragen ein Client innerhalb eines Zeitraums stellen kann. Es dämpft Angriffe durch Brute-Force-Methoden sowie Denial-of-Service-Angriffe und verhindert, dass ein besonders intensiver Nutzer 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 Quote besorgen, meist in einem kostenpflichtigen Tarif.
    • Nach Geografie oder Zeit: Die Beschrä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 Regionen mit verdächtigem Traffic strengere Beschränkungen gelten.
  • Nach Serverkapazität: Einige Teile einer API werden an spezielle Infrastrukturen mit eigenen Grenzen weitergeleitet, wie beispielsweise einen kleinen Pool an Hintergrundprozessen für aufwändige Aufgaben.
  • Es existieren viele Algorithmen (feste Fenster, schräg verlaufende Fenster, Token-Bucket-Methode), 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 eine Schlüssel zu, 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 Unternehmensproxy 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);
    

    So wie es implementiert ist, erzeugt jede Anfrage, die den Header weglässt, denselben undefined-Schlüssel und teilt sich einen einzigen Pool. In der Praxis sollten Schlüssellose Anfragen bereits früher in der Verarbeitungskette abgelehnt werden oder man sollte auf die IP-Adresse zurückgreifen.

    Beschränkung teurer Endpunkte

    Durch Throttling wird bestimmt, wie schnell Anfragen angenommen werden, damit plötzliche Spitzen 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 hier 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 Drosselung wünschen, die Clients vor der Ablehnung verlangsamt, bietet ein begleitendes Paket 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 beibehalten werden. 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 TypeScript-Alternative finden Sie unseren Artikel über einen minimalen Rate-Limiter für Express.

    6. Logging, Überwachung und Incident-Erkennung

    Man kann nicht auf einen Angriff reagieren, den man nie sieht. Protokollierung erfasst Anfragen und Antworten zusammen mit deren Metadaten, Kontext, Zeitangaben 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 sowie Fehler. Größere Systeme benötigen eine strukturierte Ausgabe und zentrale Speicherung, um diese Informationen nutzbar zu machen.
  • Metriken mit Prometheus und Grafana überwachen die Anfragenrate, Latenzzeit, CPU-Auslastung, Speicherverbrauch sowie Fehlerraten. Sie zeigen aggregierte Trends an, aber nicht, was in einer einzelnen Anfrage geschah.
  • Zentrale Protokollverwaltung mit dem ELK Stack, OpenSearch, Splunk oder CloudWatch bündelt Protokolle aus verschiedenen Diensten – allerdings zu Lasten zusätzlicher Infrastruktur und operativer Aufwände.
  • Überwachung der Anwendungsleistung mit Datadog, New Relic oder Dynatrace vereint Protokolle, Metriken, Trace-Daten sowie Alarmfunktionen in einer Plattform – allerdings zu höheren Lizenzkosten und größerer Komplexität.
  • Jeder dieser Komponenten kann in Express eingebunden werden. Im Folgenden werden vier gängige Bausteine vorgestellt.

    Anfragen protokollieren mit Morgan

    Durch die Registrierung von Morgan im combined-Format wird für jede Anfrage eine in Apache-Stil 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 bei Erstellung einer Bestellung erzeugt eine Prüfungsspur, die später durchsucht werden kann.

    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 Protokolle aufgenommen wird. Benutzeridentifikatoren sind in Ordnung; Passwörter, Tokens, vollständige Kartennummern sowie gesamte Anfragekörper hingegen nicht – Protokolle sind ein häufiger Ort für das Lecken sensibler Daten.

    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 Meldung 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 standardmäßige Prozessmetriken von Node.js und stellt sie über einen /metrics-Endpunkt zur Verfügung, damit Prometheus diese 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());
    });
    

    Der 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.

    Entscheidungsfreudige Frameworks helfen hier. NestJS kommt mit einem integrierten Logger sowie einer Struktur, die es ermöglicht, für jeden Endpunkt leicht Logging- und Metrikenfunktionen hinzuzufügen, während man bei einem unentschlossenen Framework 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 der 401-Antworten oder Ablehnungen aufgrund von Rate-Limits, werden an den Benachrichtigungskanal weitergeleitet, 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 ist ein OAuth-Verfahren oder ein äquivalenter Token-Fluss implementiert.
    • Ausgestellte JWTs haben eine Ablaufzeit.
    • Limits schützen alle Routen, wobei bei der Anmeldeseite strengere Limits gelten.
  • Jeder Eingabewert wird vor der Verwendung überprüft.
  • Abfragen wurden auf Angriffe durch Dateninjektion getestet.
  • Access-Regeln wurden getestet, einschließlich objektspezifischer Überprüfungen.
  • Schlüssel und Geheimnisse befinden sich außerhalb der Codebasis.
  • 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.
  • Zur Veranschaulichung 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 Anfragenvalidierung über Data Transfer Objects. Die Dokumentation Ihres Frameworks zeigt Ihnen die übliche Umsetzung für jede Schicht.

    Dieselben Prinzipien bilden auch die Grundlage für cloud-native Deployment-Modelle auf Plattformen wie Azure, Google Cloud und AWS, die darüber hinaus eigene Gateways, Identitätsdienste sowie verwaltete Rate-Limiting-Funktionen bereitstellen. Cloud-spezifische Vorgehensweisen verdienen eine eigene Behandlung, doch die oben genannten Schichten bringen eine API bereits weitgehend auf das Niveau, um eine Sicherheitsprüfung zu bestehen.

    Kernpunkte

    • Trennen Sie die Überprüfung von Zugangsdaten von der Authentifizierung pro Anfrage: Überprüfen Sie das Passwort einmal und verlassen Sie sich anschließend auf kurzlebige, 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.
    • Für ruhendes Datenmaterial verwenden Sie AES-GCM mit einem einzigartigen IV pro Operation; reservieren Sie asymmetrische Verschlüsselung für kleine Werte und Schlüsselaustausch.
  • Überprüfung des Content-Typs der Schichten, Validierung, Sanitisierung sowie parametrisierte Abfragen – dabei sollte man auch die Reihenfolge ihrer Ausführung berücksichtigen.
  • Setzen Sie für alles eine Rate-Limitung, wenden Sie strengere Limits auf Anmeldeseiten und ressourcenintensive Endpunkte an, und nutzen Sie einen gemeinsamen Speicher, wenn mehr als eine Instanz betrieben wird.
  • Protokollieren Sie sicherheitsrelevante Ereignisse, ohne Geheimnisse mit aufzunehmen, und wandeln Sie diese Protokolle in Warnungen um, auf die jemand reagieren kann.
  • Weitere Literatur