Skip to content

Sicherheitsbewährte Praktiken

Absicht

Dieses Dokument soll das aktuelle Bedrohungsmodell erweitern und umfassende Richtlinien zur Sicherung einer Node.js-Anwendung bereitstellen.

Dokumentinhalt

  • Bewährte Praktiken: Eine vereinfachte, zusammengefasste Darstellung der bewährten Praktiken. Wir können dieses Problem oder diese Richtlinie als Ausgangspunkt verwenden. Es ist wichtig zu beachten, dass dieses Dokument spezifisch für Node.js ist. Wenn Sie etwas Umfassendes suchen, ziehen Sie die OSSF Best Practices in Betracht.
  • Erklärte Angriffe: Veranschaulichen und dokumentieren Sie in einfachem Englisch mit einigen Codebeispielen (wenn möglich) die Angriffe, die wir im Bedrohungsmodell erwähnen.
  • Bibliotheken von Drittanbietern: Definieren Sie Bedrohungen (Typosquatting-Angriffe, bösartige Pakete...) und bewährte Praktiken in Bezug auf Node-Modulabhängigkeiten usw.

Bedrohungsliste

Denial of Service des HTTP-Servers (CWE-400)

Dies ist ein Angriff, bei dem die Anwendung aufgrund der Art und Weise, wie sie eingehende HTTP-Anfragen verarbeitet, für den Zweck, für den sie entwickelt wurde, nicht mehr verfügbar ist. Diese Anfragen müssen nicht absichtlich von einem böswilligen Akteur erstellt worden sein: Ein falsch konfigurierter oder fehlerhafter Client kann auch ein Muster von Anfragen an den Server senden, das zu einem Denial of Service führt.

HTTP-Anfragen werden vom Node.js-HTTP-Server empfangen und über den registrierten Anfrage-Handler an den Anwendungscode übergeben. Der Server analysiert den Inhalt des Anfragekörpers nicht. Daher ist ein DoS, der durch den Inhalt des Körpers verursacht wird, nachdem er an den Anfrage-Handler übergeben wurde, keine Schwachstelle in Node.js selbst, da es in der Verantwortung des Anwendungscodes liegt, ihn korrekt zu behandeln.

Stellen Sie sicher, dass der Webserver Socket-Fehler ordnungsgemäß behandelt, z. B. wenn ein Server ohne Fehlerbehandlung erstellt wird, ist er anfällig für DoS.

javascript
import net from 'node:net'
const server = net.createServer(socket => {
  // socket.on('error', console.error) // dies verhindert das Abstürzen des Servers
  socket.write('Echo server\r\n')
  socket.pipe(socket)
})
server.listen(5000, '0.0.0.0')

Wenn eine fehlerhafte Anfrage durchgeführt wird, kann der Server abstürzen.

Ein Beispiel für einen DoS-Angriff, der nicht durch den Inhalt der Anfrage verursacht wird, ist Slowloris. Bei diesem Angriff werden HTTP-Anfragen langsam und fragmentiert, ein Fragment nach dem anderen, gesendet. Bis die vollständige Anfrage zugestellt ist, behält der Server Ressourcen für die laufende Anfrage reserviert. Wenn genügend dieser Anfragen gleichzeitig gesendet werden, erreicht die Anzahl der gleichzeitigen Verbindungen bald ihr Maximum, was zu einem Denial of Service führt. So hängt der Angriff nicht vom Inhalt der Anfrage ab, sondern vom Timing und Muster der an den Server gesendeten Anfragen.

Abschwächungen

  • Verwenden Sie einen Reverse-Proxy, um Anfragen zu empfangen und an die Node.js-Anwendung weiterzuleiten. Reverse-Proxys können Caching, Load Balancing, IP-Blacklisting usw. bereitstellen, was die Wahrscheinlichkeit verringert, dass ein DoS-Angriff wirksam ist.
  • Konfigurieren Sie die Server-Timeouts korrekt, sodass Verbindungen, die im Leerlauf sind oder bei denen Anfragen zu langsam eintreffen, abgebrochen werden können. Siehe die verschiedenen Timeouts in http.Server, insbesondere headersTimeout, requestTimeout, timeout und keepAliveTimeout.
  • Begrenzen Sie die Anzahl der offenen Sockets pro Host und insgesamt. Siehe die http-Dokumente, insbesondere agent.maxSockets, agent.maxTotalSockets, agent.maxFreeSockets und server.maxRequestsPerSocket.

DNS-Rebinding (CWE-346)

Dies ist ein Angriff, der auf Node.js-Anwendungen abzielen kann, die mit dem Debugging-Inspektor aktiviert sind, unter Verwendung des Schalters --inspect.

Da in einem Webbrowser geöffnete Websites WebSocket- und HTTP-Anfragen stellen können, können sie auf den lokal laufenden Debugging-Inspektor abzielen. Dies wird normalerweise durch die Same-Origin-Policy verhindert, die von modernen Browsern implementiert wird und die verhindert, dass Skripte auf Ressourcen mit unterschiedlichen Ursprüngen zugreifen (d. h. eine bösartige Website kann keine Daten lesen, die von einer lokalen IP-Adresse angefordert werden).

Durch DNS-Rebinding kann ein Angreifer jedoch die Herkunft für seine Anfragen vorübergehend steuern, sodass sie scheinbar von einer lokalen IP-Adresse stammen. Dies geschieht durch die Kontrolle sowohl einer Website als auch des DNS-Servers, der zur Auflösung ihrer IP-Adresse verwendet wird. Weitere Informationen finden Sie im DNS Rebinding Wiki.

Abschwächungen

  • Deaktivieren Sie den Inspektor beim SIGUSR1-Signal, indem Sie einen process.on('SIGUSR1', ...)-Listener daran anhängen.
  • Führen Sie das Inspektorenprotokoll nicht in der Produktion aus.

Offenlegung sensibler Informationen gegenüber einem nicht autorisierten Akteur (CWE-552)

Alle Dateien und Ordner, die im aktuellen Verzeichnis enthalten sind, werden während der Paketveröffentlichung in die npm-Registry übertragen.

Es gibt einige Mechanismen, um dieses Verhalten zu steuern, indem eine Sperrliste mit .npmignore und .gitignore definiert wird oder eine Zulassungsliste in der package.json definiert wird.

Gegenmaßnahmen

  • Verwenden Sie npm publish --dry-run, um alle zu veröffentlichenden Dateien aufzulisten. Stellen Sie sicher, dass Sie den Inhalt überprüfen, bevor Sie das Paket veröffentlichen.
  • Es ist auch wichtig, Ignorierdateien wie .gitignore und .npmignore zu erstellen und zu pflegen. In diesen Dateien können Sie angeben, welche Dateien/Ordner nicht veröffentlicht werden sollen. Die Eigenschaft files in package.json ermöglicht die umgekehrte Operation – eine Liste der -- erlaubten Dateien.
  • Im Falle einer Offenlegung stellen Sie sicher, dass Sie das Paket wieder entfernen.

HTTP-Anfrage-Schmuggel (CWE-444)

Dies ist ein Angriff, bei dem zwei HTTP-Server beteiligt sind (in der Regel ein Proxy und eine Node.js-Anwendung). Ein Client sendet eine HTTP-Anfrage, die zuerst über den Front-End-Server (den Proxy) geht und dann an den Back-End-Server (die Anwendung) weitergeleitet wird. Wenn der Front-End und der Back-End uneindeutige HTTP-Anfragen unterschiedlich interpretieren, besteht die Möglichkeit, dass ein Angreifer eine bösartige Nachricht sendet, die vom Front-End nicht gesehen wird, aber vom Back-End gesehen wird, wodurch sie effektiv am Proxy-Server "vorbeigeschmuggelt" wird.

Weitere Informationen und Beispiele finden Sie unter CWE-444.

Da dieser Angriff davon abhängt, dass Node.js HTTP-Anfragen anders interpretiert als ein (beliebiger) HTTP-Server, kann ein erfolgreicher Angriff auf eine Schwachstelle in Node.js, dem Front-End-Server oder beiden zurückzuführen sein. Wenn die Art und Weise, wie die Anfrage von Node.js interpretiert wird, mit der HTTP-Spezifikation übereinstimmt (siehe RFC7230), dann wird dies nicht als Schwachstelle in Node.js angesehen.

Gegenmaßnahmen

  • Verwenden Sie die Option insecureHTTPParser nicht, wenn Sie einen HTTP-Server erstellen.
  • Konfigurieren Sie den Front-End-Server so, dass er uneindeutige Anfragen normalisiert.
  • Überwachen Sie kontinuierlich auf neue HTTP-Anfrage-Schmuggel-Schwachstellen sowohl in Node.js als auch im Front-End-Server Ihrer Wahl.
  • Verwenden Sie nach Möglichkeit durchgehend HTTP/2 und deaktivieren Sie HTTP-Downgrading.

Informationspreisgabe durch Timing-Angriffe (CWE-208)

Dies ist ein Angriff, der es dem Angreifer ermöglicht, potenziell sensible Informationen zu erfahren, indem er beispielsweise misst, wie lange es dauert, bis die Anwendung auf eine Anfrage antwortet. Dieser Angriff ist nicht spezifisch für Node.js und kann auf fast alle Laufzeitumgebungen abzielen.

Der Angriff ist immer dann möglich, wenn die Anwendung ein Geheimnis in einer zeitkritischen Operation (z. B. einer Verzweigung) verwendet. Betrachten Sie die Behandlung der Authentifizierung in einer typischen Anwendung. Hier beinhaltet eine grundlegende Authentifizierungsmethode E-Mail und Passwort als Anmeldeinformationen. Benutzerinformationen werden aus der Eingabe des Benutzers idealerweise aus einem DBMS abgerufen. Nach dem Abrufen der Benutzerinformationen wird das Passwort mit den aus der Datenbank abgerufenen Benutzerinformationen verglichen. Die Verwendung des eingebauten Stringvergleichs benötigt für gleichlange Werte mehr Zeit. Dieser Vergleich erhöht, wenn er für eine akzeptable Menge ausgeführt wird, ungewollt die Antwortzeit der Anfrage. Durch den Vergleich der Antwortzeiten der Anfragen kann ein Angreifer die Länge und den Wert des Passworts in einer großen Anzahl von Anfragen erraten.

Gegenmaßnahmen

  • Die Krypto-API stellt eine Funktion timingSafeEqual bereit, um tatsächliche und erwartete sensible Werte mit einem Algorithmus mit konstanter Laufzeit zu vergleichen.
  • Für den Passwortvergleich können Sie das scrypt verwenden, das auch im nativen Krypto-Modul verfügbar ist.
  • Vermeiden Sie im Allgemeinen die Verwendung von Geheimnissen in Operationen mit variabler Laufzeit. Dies umfasst die Verzweigung von Geheimnissen und, wenn sich der Angreifer auf derselben Infrastruktur befinden könnte (z. B. derselbe Cloud-Rechner), die Verwendung eines Geheimnisses als Index in den Speicher. Das Schreiben von Code mit konstanter Laufzeit in JavaScript ist schwierig (teilweise wegen des JIT). Verwenden Sie für Krypto-Anwendungen die integrierten Krypto-APIs oder WebAssembly (für Algorithmen, die nicht nativ implementiert sind).

Böswillige Drittanbietermodule (CWE-1357)

Derzeit kann in Node.js jedes Paket auf leistungsstarke Ressourcen wie den Netzwerkzugriff zugreifen. Da sie außerdem Zugriff auf das Dateisystem haben, können sie beliebige Daten überallhin senden.

Jeder Code, der in einem Node-Prozess ausgeführt wird, hat die Möglichkeit, zusätzlichen beliebigen Code zu laden und auszuführen, indem er eval() (oder seine Äquivalente) verwendet. Jeder Code mit Dateisystem-Schreibzugriff kann dasselbe erreichen, indem er in neue oder vorhandene Dateien schreibt, die geladen werden.

Node.js verfügt über einen experimentellen¹ Richtlinienmechanismus, um die geladene Ressource als nicht vertrauenswürdig oder vertrauenswürdig zu deklarieren. Diese Richtlinie ist jedoch standardmäßig nicht aktiviert. Stellen Sie sicher, dass Sie Versionsnummern der Abhängigkeiten festlegen und automatische Überprüfungen auf Sicherheitslücken mithilfe allgemeiner Workflows oder npm-Skripte durchführen. Bevor Sie ein Paket installieren, stellen Sie sicher, dass dieses Paket gepflegt wird und alle erwarteten Inhalte enthält. Seien Sie vorsichtig, der GitHub-Quellcode ist nicht immer derselbe wie der veröffentlichte, validieren Sie ihn in node_modules.

Lieferkettenangriffe

Ein Lieferkettenangriff auf eine Node.js-Anwendung erfolgt, wenn eine ihrer Abhängigkeiten (direkte oder transitive) kompromittiert wird. Dies kann entweder dadurch geschehen, dass die Anwendung bei der Spezifizierung der Abhängigkeiten zu nachlässig ist (wodurch unerwünschte Aktualisierungen zugelassen werden) und/oder durch häufige Tippfehler in der Spezifikation (anfällig für Typosquatting).

Ein Angreifer, der die Kontrolle über ein Upstream-Paket übernimmt, kann eine neue Version mit bösartigem Code darin veröffentlichen. Wenn eine Node.js-Anwendung von diesem Paket abhängt, ohne streng darauf zu achten, welche Version sicher zu verwenden ist, kann das Paket automatisch auf die neueste bösartige Version aktualisiert werden, wodurch die Anwendung kompromittiert wird.

Abhängigkeiten, die in der Datei package.json angegeben sind, können eine exakte Versionsnummer oder einen Bereich haben. Wenn jedoch eine Abhängigkeit an eine exakte Version angeheftet wird, werden ihre transitiven Abhängigkeiten nicht selbst angeheftet. Dies macht die Anwendung weiterhin anfällig für unerwünschte/unerwartete Aktualisierungen.

Mögliche Angriffsvektoren:

  • Typosquatting-Angriffe
  • Lockfile-Vergiftung
  • Kompromittierte Betreuer
  • Bösartige Pakete
  • Abhängigkeitsverwirrungen
Gegenmaßnahmen
  • Verhindern Sie, dass npm beliebige Skripte mit --ignore-scripts ausführt.
    • Zusätzlich können Sie dies global mit npm config set ignore-scripts true deaktivieren.
  • Heften Sie Versionsabhängigkeiten an eine bestimmte, unveränderliche Version, nicht an eine Version, die ein Bereich oder aus einer veränderlichen Quelle stammt.
  • Verwenden Sie Lockfiles, die jede Abhängigkeit (direkt und transitiv) anheften.
  • Automatisieren Sie die Überprüfung auf neue Schwachstellen mit CI, mit Tools wie npm-audit.
    • Tools wie Socket können verwendet werden, um Pakete mit statischer Analyse zu analysieren, um riskantes Verhalten wie Netzwerk- oder Dateisystemzugriff zu finden.
  • Verwenden Sie npm ci anstelle von npm install. Dies erzwingt das Lockfile, sodass Inkonsistenzen zwischen ihm und der package.json-Datei einen Fehler verursachen (anstatt das Lockfile stillschweigend zugunsten von package.json zu ignorieren).
  • Überprüfen Sie die Datei package.json sorgfältig auf Fehler/Tippfehler in den Namen der Abhängigkeiten.

Speicherzugriffsverletzung (CWE-284)

Speicherbasierte oder Heap-basierte Angriffe hängen von einer Kombination aus Fehlern bei der Speicherverwaltung und einem ausnutzbaren Speicherallokator ab. Wie alle Laufzeiten ist auch Node.js anfällig für diese Angriffe, wenn Ihre Projekte auf einem gemeinsam genutzten Rechner laufen. Die Verwendung eines sicheren Heaps ist nützlich, um zu verhindern, dass sensible Informationen durch Zeigerüberläufe und -unterläufe verloren gehen.

Leider ist ein sicherer Heap unter Windows nicht verfügbar. Weitere Informationen finden Sie in der Node.js secure-heap-Dokumentation.

Abschwächungen

  • Verwenden Sie --secure-heap=n, abhängig von Ihrer Anwendung, wobei n die zugewiesene maximale Bytegröße ist.
  • Führen Sie Ihre Produktionsanwendung nicht auf einem gemeinsam genutzten Rechner aus.

Monkey Patching (CWE-349)

Monkey Patching bezeichnet die Modifikation von Eigenschaften zur Laufzeit mit dem Ziel, das bestehende Verhalten zu ändern. Beispiel:

js
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
  // Überschreiben der globalen [].push
}

Abschwächungen

Das Flag --frozen-intrinsics aktiviert experimentelle¹ eingefrorene Intrinsiken, was bedeutet, dass alle eingebauten JavaScript-Objekte und -Funktionen rekursiv eingefroren werden. Daher wird der folgende Codeausschnitt das Standardverhalten von Array.prototype.push nicht überschreiben.

js
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
  // Überschreiben der globalen [].push
}
// Uncaught:
// TypeError <Object <Object <[Object: null prototype] {}>>>:
// Cannot assign to read only property 'push' of object '

Es ist jedoch wichtig zu erwähnen, dass Sie immer noch neue Globals definieren und bestehende Globals mit globalThis ersetzen können.

bash
globalThis.foo = 3; foo; // Sie können immer noch neue Globals definieren 3
globalThis.Array = 4; Array; // Sie können aber auch bestehende Globals ersetzen 4

Daher kann Object.freeze(globalThis) verwendet werden, um zu gewährleisten, dass keine Globals ersetzt werden.

Prototypen-Verschmutzungsangriffe (CWE-1321)

Prototypenverschmutzung bezieht sich auf die Möglichkeit, Eigenschaften in Javascript-Sprachelemente zu ändern oder einzuschleusen, indem die Verwendung von _proto, _constructor, Prototypen und anderen von eingebauten Prototypen geerbten Eigenschaften missbraucht wird.

js
const a = { a: 1, b: 2 }
const data = JSON.parse('{"__proto__": { "polluted": true}}')
const c = Object.assign({}, a, data)
console.log(c.polluted) // true
// Potenzieller DoS
const data2 = JSON.parse('{"__proto__": null}')
const d = Object.assign(a, data2)
d.hasOwnProperty('b') // Uncaught TypeError: d.hasOwnProperty is not a function

Dies ist eine potenzielle Schwachstelle, die von der JavaScript-Sprache geerbt wird.

Beispiele

Abschwächungsmaßnahmen

  • Vermeiden Sie unsichere rekursive Zusammenführungen, siehe CVE-2018-16487.
  • Implementieren Sie JSON-Schema-Validierungen für externe/nicht vertrauenswürdige Anfragen.
  • Erstellen Sie Objekte ohne Prototyp mit Object.create(null).
  • Frieren Sie den Prototyp ein: Object.freeze(MyObject.prototype).
  • Deaktivieren Sie die Eigenschaft Object.prototype.__proto__ mit dem Flag --disable-proto.
  • Überprüfen Sie mit Object.hasOwn(obj, keyFromObj), ob die Eigenschaft direkt im Objekt und nicht im Prototyp vorhanden ist.
  • Vermeiden Sie die Verwendung von Methoden aus Object.prototype.

Unkontrolliertes Suchpfadelement (CWE-427)

Node.js lädt Module gemäß dem Modulauflösungsalgorithmus. Daher geht es davon aus, dass das Verzeichnis, in dem ein Modul angefordert (require) wird, vertrauenswürdig ist.

Das bedeutet, dass das folgende Anwendungsverhalten erwartet wird. Angenommen, die folgende Verzeichnisstruktur:

  • app/
    • server.js
    • auth.js
    • auth

Wenn server.js require('./auth') verwendet, folgt es dem Modulauflösungsalgorithmus und lädt auth anstelle von auth.js.

Abschwächungsmaßnahmen

Die Verwendung des experimentellen¹ Richtlinienmechanismus mit Integritätsprüfung kann die oben genannte Bedrohung vermeiden. Für das oben beschriebene Verzeichnis kann man die folgende policy.json verwenden.

json
{
  "resources": {
    "./app/auth.js": {
      "integrity": "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8="
    },
    "./app/server.js": {
      "dependencies": {
        "./auth": "./app/auth.js"
      },
      "integrity": "sha256-NPtLCQ0ntPPWgfVEgX46ryTNpdvTWdQPoZO3kHo0bKI="
    }
  }
}

Wenn also das Auth-Modul angefordert wird, validiert das System die Integrität und gibt einen Fehler aus, wenn diese nicht mit der erwarteten übereinstimmt.

bash
» node --experimental-policy=policy.json app/server.js
node:internal/policy/sri:65
      throw new ERR_SRI_PARSE(str, str[prevIndex], prevIndex);
      ^
SyntaxError [ERR_SRI_PARSE]: Subresource Integrity string "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8=%" had an unexpected "%" at position 51
    at new NodeError (node:internal/errors:393:5)
    at Object.parse (node:internal/policy/sri:65:13)
    at processEntry (node:internal/policy/manifest:581:38)
    at Manifest.assertIntegrity (node:internal/policy/manifest:588:32)
    at Module._compile (node:internal/modules/cjs/loader:1119:21)
    at Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
    at Module.load (node:internal/modules/cjs/loader:1037:32)
    at Module._load (node:internal/modules/cjs/loader:878:12)
    at Module.require (node:internal/modules/cjs/loader:1061:19)
    at require (node:internal/modules/cjs/helpers:99:18) {
  code: 'ERR_SRI_PARSE'
}

Beachten Sie, dass es immer empfehlenswert ist, --policy-integrity zu verwenden, um Richtlinienänderungen zu vermeiden.

Experimentelle Funktionen in der Produktion

Die Verwendung experimenteller Funktionen in der Produktion wird nicht empfohlen. Experimentelle Funktionen können bei Bedarf zu grundlegenden Änderungen führen, und ihre Funktionalität ist nicht sicher stabil. Feedback wird jedoch sehr geschätzt.

OpenSSF-Tools

Die OpenSSF leitet mehrere Initiativen, die sehr nützlich sein können, insbesondere wenn Sie planen, ein npm-Paket zu veröffentlichen. Diese Initiativen umfassen:

  • OpenSSF Scorecard Scorecard bewertet Open-Source-Projekte anhand einer Reihe automatisierter Sicherheitsrisikoprüfungen. Sie können es verwenden, um proaktiv Schwachstellen und Abhängigkeiten in Ihrer Codebasis zu bewerten und fundierte Entscheidungen über die Akzeptanz von Schwachstellen zu treffen.
  • OpenSSF Best Practices Badge Program Projekte können sich freiwillig selbst zertifizieren, indem sie beschreiben, wie sie jede bewährte Methode einhalten. Dadurch wird ein Badge generiert, der dem Projekt hinzugefügt werden kann.