Sicherheits-Spickzettel: Nodejs



Über die Popularität von NodeJS wurde bereits viel gesagt. Die Zunahme der Anzahl der Anwendungen ist offensichtlich - NodeJS ist recht einfach zu erlernen, verfügt über eine große Anzahl von Bibliotheken sowie ein sich dynamisch entwickelndes Ökosystem.

Wir haben Empfehlungen für NodeJS-Entwickler basierend auf OWASP- Spickzettel erstellt , damit Sie Sicherheitsprobleme bei der Entwicklung von Anwendungen vorhersehen können.

Sicherheitsempfehlungen für NodeJS-Anwendungen können in die folgenden Kategorien unterteilt werden:

  • Sicherheit während der Anwendungsentwicklung;
  • Serversicherheit;
  • Plattformsicherheit;


Anwendungsentwicklungssicherheit


Vermeiden Sie

Rückrufhölle Die Verwendung von Rückruffunktionen (Rückrufen) ist eine der größten Stärken von NodeJS. Wenn Sie jedoch Rückrufe verschachteln, können Sie leicht vergessen, den Fehler in einer der Funktionen zu behandeln. Eine Möglichkeit, die Rückrufhölle zu vermeiden, besteht darin, Versprechen zu verwenden. Auch wenn das von Ihnen verwendete Modul das Arbeiten mit Versprechungen nicht unterstützt, können Sie Promise.promisifyAll () jederzeit verwenden. Aber auch mit Versprechungen lohnt es sich, auf das Verschachteln zu achten. Um den Rückruf-Höllenfehler vollständig zu vermeiden, halten Sie sich an eine „flache“ Kette von Versprechungen.

Beispiel für eine Rückrufhölle:

function func1(name, callback) {
   setTimeout(function() {
      // operations
   }, 500);
}
function func2(name, callback) {
   setTimeout(function() {
      // operations
   }, 100);
}
function func3(name, callback) {
   setTimeout(function() {
      // operations
   }, 900);
}
function func4(name, callback) {
   setTimeout(function() {
      // operations
   }, 3000);
}

func1("input1", function(err, result1){
   if(err){
      // error operations
   }
   else {
      //some operations
      func2("input2", function(err, result2){
         if(err){
            //error operations
         }
         else{
            //some operations
            func3("input3", function(err, result3){
               if(err){
                  //error operations
               }
               else{
                  // some operations
                  func4("input 4", function(err, result4){
                     if(err){
                        // error operations
                     }
                     else {
                        // some operations
                     }
                  });
               }
            });
         }
      });
   }
});

Gleicher Code mit Flachkettenversprechen:

function func1(name, callback) {
   setTimeout(function() {
      // operations
   }, 500);
}
function func2(name, callback) {
   setTimeout(function() {
      // operations
   }, 100);
}
function func3(name, callback) {
   setTimeout(function() {
      // operations
   }, 900);
}
function func4(name, callback) {
   setTimeout(function() {
      // operations
   }, 3000);
}

func1("input1")
   .then(function (result){
      return func2("input2");
   })
   .then(function (result){
      return func3("input3");
   })
   .then(function (result){
      return func4("input4");
   })
   .catch(function (error) {
      // error operations
   });

Begrenzen Sie die Größe der Anforderung. Das

Parsen des Anforderungshauptteils kann sehr ressourcenintensiv sein. Wenn Sie die Größe der Anforderung nicht einschränken, können Angreifer ausreichend große Anforderungen senden, die den gesamten Speicherplatz ausfüllen oder alle Serverressourcen erschöpfen können. Gleichzeitig kann es jedoch falsch sein, die Anforderungsgröße für alle Fälle zu begrenzen, da Anforderungen wie das Herunterladen einer Datei vorliegen. Daher wird empfohlen, Grenzwerte für verschiedene Arten von Inhalten festzulegen. Mit dem Express-Framework kann dies beispielsweise wie folgt implementiert werden:

app.use(express.urlencoded({ limit: "1kb" }));
app.use(express.json({ limit: "1kb" }));
app.use(express.multipart({ limit:"10mb" }));

Es ist zu beachten, dass ein Angreifer den Typ des Anforderungsinhalts ändern und Einschränkungen umgehen kann. Daher muss überprüft werden, ob der Inhalt der Anforderung mit dem im Anforderungsheader angegebenen Inhaltstyp übereinstimmt. Wenn die Überprüfung des Inhaltstyps die Leistung beeinträchtigt, können Sie nur bestimmte Typen oder Abfragen überprüfen, die größer als eine bestimmte Größe sind.

Blockieren Sie die Ereignisschleife nicht

Eine wichtige Komponente der Sprache ist die Ereignisschleife, mit der Sie lediglich den Ausführungskontext wechseln können, ohne auf den Abschluss des Vorgangs warten zu müssen. Es gibt jedoch Blockierungsvorgänge, deren Abschluss NodeJS warten muss, bevor der Code fortgesetzt wird. Beispielsweise blockieren die meisten synchronen Methoden:

const fs = require('fs');
fs.unlinkSync('/file.txt');

Es wird empfohlen, solche Vorgänge asynchron auszuführen:

const fs = require('fs');
fs.unlink('/file.txt', (err) => {
    if (err) throw err;
});

Vergessen Sie gleichzeitig nicht, dass der nach dem asynchronen Aufruf stehende Code ausgeführt wird, ohne auf den Abschluss des vorherigen Vorgangs zu warten.

Im folgenden Code wird die Datei beispielsweise vor dem Lesen gelöscht, was zu einer Rennbedingung führen kann.

const fs = require('fs');
fs.readFile('/file.txt', (err, data) => {
  // perform actions on file content
});
fs.unlinkSync('/file.txt');

Um dies zu vermeiden, können Sie alle Operationen in einer nicht blockierenden Funktion schreiben:

const fs = require('fs');
fs.readFile('/file.txt', (err, data) => {
  // perform actions on file content
  fs.unlink('/file.txt', (err) => {
    if (err) throw err;
  });
});

Eingabefelder überprüfen Das

Überprüfen von Eingabefeldern ist ein wichtiger Bestandteil der Sicherheit jeder Anwendung. Validierungsfehler können dazu führen, dass Ihre Anwendung sofort für viele Arten von Angriffen anfällig wird: SQL-Injection, xss, Command Injection und andere. Um die Formularvalidierung zu vereinfachen, können Sie Validator-Pakete verwenden, mongo-express-sanitize.

Escape-Benutzerdaten

Eine der Regeln, die Ihnen helfen, sich vor xss-Angriffen zu schützen, besteht darin, Benutzerdaten zu schützen. Sie können hierfür die Escape-HTML- oder Node-Esapi-Bibliothek verwenden.

Protokolle führen

Darüber hinaus hilft es beim Debuggen von Fehlern. Die Protokollierung kann verwendet werden, um auf Vorfälle zu reagieren. Weitere Informationen zur Notwendigkeit der Protokollierung finden Sie hier.. Eines der beliebtesten NodeJS-Protokollierungspakete ist Winston und Bunyan. Das folgende Beispiel zeigt, wie Sie mit Winston Protokolle sowohl an die Konsole als auch an die Datei ausgeben:

var logger = new (Winston.Logger) ({
    transports: [
        new (winston.transports.Console)(),
        new (winston.transports.File)({ filename: 'application.log' })
    ],
    level: 'verbose'
});

Steuern des Ereigniszyklus

Wenn sich auf Ihrem Server ein intensiver Netzwerkverkehr befindet, können Benutzer Schwierigkeiten mit der Verfügbarkeit Ihres Dienstes haben. Dies ist im Wesentlichen ein DoS-Angriff. In diesem Fall können Sie die Antwortzeit verfolgen und, wenn sie die angegebene Zeit überschreitet, eine Nachricht an 503 Server Too Busy senden. Das Modul toobusy-js kann helfen.

Ein Beispiel für die Verwendung des Moduls:

var toobusy = require('toobusy-js');
var express = require('express');
var app = express();
app.use(function(req, res, next) {
    if (toobusy()) {
        // log if you see necessary
        res.send(503, "Server Too Busy");
    } else {
    next();
    }
});

Treffen Sie Vorsichtsmaßnahmen gegen rohe Gewalt.

Auch hier kommen Module zur Rettung. Zum Beispiel Express-Brute oder Express-Bouncer. Anwendungsbeispiel:

var bouncer = require('express-bouncer');
bouncer.whitelist.push('127.0.0.1'); // whitelist an IP address
// give a custom error message
bouncer.blocked = function (req, res, next, remaining) {
    res.send(429, "Too many requests have been made. Please wait " + remaining/1000 + " seconds.");
};
// route to protect
app.post("/login", bouncer.block, function(req, res) {
    if (LoginFailed){  }
    else {
        bouncer.reset( req );
    }
});

Die Verwendung von CAPTCHA ist eine weitere gängige Brute-Force-Gegenmaßnahme. Ein häufig verwendetes Modul zur Implementierung von CAPTCHA ist svg-captcha.

Verwenden von CSRF-Token

Eine der zuverlässigsten Möglichkeiten zum Schutz vor CSRF-Angriffen ist die Verwendung eines CSRF-Tokens. Das Token muss mit hoher Entropie generiert, streng überprüft und an die Sitzung des Benutzers gebunden werden. Um den Betrieb des CSRF-Tokens sicherzustellen, können Sie das csurf-Modul verwenden.

Anwendungsbeispiel:

var csrf = require('csurf');
csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, function(req, res) {
    res.render('send', { csrfToken: req.csrfToken() })
})
app.post('/process', parseForm, csrfProtection, function(req, res) {
    res.send('data is being processed');
});

Vergessen Sie nicht, das Token dem ausgeblendeten Feld auf der Seite hinzuzufügen:

<input type="hidden" name="_csrf" value="{{ csrfToken }}">

Weitere Informationen zu CSRF-Token finden Sie in unserem Artikel .

Löschen Sie unnötige Routen. Die

Webanwendung sollte keine Seiten enthalten, die nicht von Benutzern verwendet werden, da dies die Angriffsfläche vergrößern kann. Daher müssen alle nicht verwendeten API-Routen deaktiviert werden. Sie sollten diese Frage besonders beachten, wenn Sie Sails- oder Feathers-Frameworks verwenden, da diese automatisch API-Endpunkte generieren.

Schützen Sie sich vor HPP (HTTP Parameter Pollution)

Standardmäßig fügt express alle Parameter aus der Anforderung zu einem Array hinzu. OWASP empfiehlt die Verwendung des hpp-Moduls, das alle Parameterwerte aus req.query und / oder req.body ignoriert und einfach den letzten Wert aus den doppelten Werten auswählt.

var hpp = require('hpp');
app.use(hpp());

Überwachen Sie die zurückgegebenen Werte.

In der Benutzertabelle können beispielsweise wichtige Daten gespeichert werden: Kennwort, E-Mail-Adresse, Geburtsdatum usw. Daher ist es wichtig, nur die erforderlichen Daten zurückzugeben.

Zum Beispiel:

 exports.sanitizeUser = function(user) {
  return {
    id: user.id,
    username: user.username,
    fullName: user.fullName
  };
};

Verwenden Sie Deskriptoren.

Deskriptoren können verwendet werden, um das Verhalten einer Eigenschaft für verschiedene Operationen zu beschreiben: beschreibbar - ob es möglich ist, den Wert einer Eigenschaft zu ändern, aufzählbar - ob es möglich ist, eine Eigenschaft in einer for..in-Schleife zu verwenden, konfigurierbar - ob es möglich ist, eine Eigenschaft zu überschreiben. Es wird empfohlen, auf die aufgelisteten Eigenschaften zu achten, da beim Definieren der Eigenschaft eines Objekts alle diese Attribute standardmäßig auf true gesetzt sind. Sie können den Wert von Eigenschaften wie folgt ändern:

var o = {};
Object.defineProperty(o, "a", {
    writable: true,
    enumerable: true,
    configurable: true,
    value: "A"
});

Verwenden von ACLs

Acl kann dabei helfen, den Datenzugriff anhand von Rollen zu unterscheiden. Das Hinzufügen von Berechtigungen sieht beispielsweise folgendermaßen aus:

// guest is allowed to view blogs
acl.allow('guest', 'blogs', 'view')
// allow function accepts arrays as any parameter
acl.allow('member', 'blogs', ['edit', 'view', 'delete'])

Catch uncaughtException

Standardmäßig löst NodeJS im Falle einer nicht erfassten Ausnahme den aktuellen Stack-Trace aus und beendet den Ausführungsthread. Mit NodeJS können Sie dieses Verhalten jedoch anpassen. Im Falle einer nicht erfassten Ausnahme wird ein nicht erfasstes Ausnahmeereignis ausgelöst, das mithilfe des Prozessobjekts abgefangen werden kann:

process.on("uncaughtException", function(err) {
    // clean up allocated resources
    // log necessary error details to log files
    process.exit(); // exit the process to avoid unknown state
});

Beachten Sie, dass bei Auftreten einer nicht erfassten Ausnahme alle zugewiesenen Ressourcen (z. B. Dateideskriptoren und Handler) gelöscht werden müssen, bevor der Z-Prozess abgeschlossen wird, um unvorhergesehene Fehler zu vermeiden. Es wird dringend davon abgeraten, dass das Programm weiterhin ausgeführt wird, wenn eine nicht erfasste Ausnahme auftritt.

Außerdem sollte der Benutzer beim Anzeigen von Fehlermeldungen keine detaillierten Fehlerinformationen wie die Stapelverfolgung offenlegen.

Serversicherheit


Setzen Sie beim Arbeiten mit Cookies Flags für Header.

Es gibt verschiedene Flags, die vor Angriffen wie xss und csrf schützen können: httpOnly, das den Zugriff auf Cookies über Javascript verhindert; Sicher - Ermöglicht das Senden von Cookies nur über HTTPS und SameSite, wodurch festgelegt wird, ob Cookies an Ressourcen von Drittanbietern übertragen werden können.

Anwendungsbeispiel:

var session = require('express-session');
app.use(session({
    secret: 'your-secret-key',
    key: 'cookieName',
    cookie: { secure: true, httpOnly: true, path: '/user', sameSite: true}
}));

Festlegen von HTTP-Headern aus Sicherheitsgründen

Im Folgenden finden Sie Header und Beispiele für die Verbindung, um sich vor einer Reihe häufiger Angriffe zu schützen. Header werden mit dem Helmmodul festgelegt.

• Strict-Transport-Security: HTTP Strict Transport Security (HSTS) teilt dem Browser mit, dass auf die Anwendung nur über HTTPS zugegriffen werden kann

app.use(helmet.hsts()); // default configuration
app.use(helmet.hsts("<max-age>", "<includeSubdomains>")); // custom configuration

• X-Frame-Optionen: Legt fest, ob die Seite in Frame, Iframe, Einbettung oder Objekt verwendet werden kann

app.use(hemlet.xframe()); // default behavior (DENY)
helmet.xframe('sameorigin'); // SAMEORIGIN
helmet.xframe('allow-from', 'http://alloweduri.com'); //ALLOW-FROM uri

• X-XSS-Schutz: Ermöglicht dem Browser, das Laden der Seite zu beenden, wenn er einen reflektierten XSS-Angriff erkennt.

var xssFilter = require('x-xss-protection');
app.use(xssFilter());

• X-Content-Type-Options: Wird verwendet, um Angriffe mit MIME-Typen zu verhindern

app.use(helmet.noSniff());

• Content-Security-Policy: Verhindert Angriffe wie XSS und Dateninjektionsangriffe

const csp = require('helmet-csp')
app.use(csp({
   directives: {
       defaultSrc: ["'self'"],  // default value for all directives that are absent
       scriptSrc: ["'self'"],   // helps prevent XSS attacks
       frameAncestors: ["'none'"],  // helps prevent Clickjacking attacks
       imgSrc: ["'self'", "'http://imgexample.com'"],
       styleSrc: ["'none'"]
    }
}))

• Cache-Control und Pragma: Zum Verwalten des Caching kann dieser Header insbesondere für Seiten nützlich sein, die vertrauliche Daten enthalten. Beachten Sie jedoch, dass das Deaktivieren des Caching auf allen Seiten die Leistung beeinträchtigen kann.

app.use(helmet.noCache());

• X-Download-Optionen: Der Header verhindert, dass Inter Explorer heruntergeladene Dateien ausführt

app.use(helmet.ieNoOpen());

• Expect-CT: Zertifikatstransparenz - Ein Mechanismus, der entwickelt wurde, um einige Probleme mit der SSL-Zertifikatinfrastruktur zu lösen. Dieser Header informiert den Browser über die Notwendigkeit einer zusätzlichen Zertifikatüberprüfung in CT-Protokollen

var expectCt = require('expect-ct');
app.use(expectCt({ maxAge: 123 }));
app.use(expectCt({ enforce: true, maxAge: 123 }));
app.use(expectCt({ enforce: true, maxAge: 123, reportUri: 'http://example.com'}));

• X-Powered-By: Ein optionaler Header, der die auf dem Server verwendete Technologie angibt. Sie können diesen Header wie folgt ausblenden:

app.use(helmet.hidePoweredBy());

Darüber hinaus können Sie den Wert ändern, um echte Informationen zu den von Ihnen verwendeten Technologien auszublenden:

app.use(helmet.hidePoweredBy({ setTo: 'PHP 4.2.0' }));

Plattformsicherheit


Aktualisieren Sie Ihre Pakete Die

Sicherheit Ihrer Anwendung hängt von der Sicherheit der von Ihnen verwendeten Pakete ab. Daher ist es wichtig, die neueste Version des Pakets zu verwenden. Um sicherzustellen, dass das von Ihnen verwendete Paket keine bekannten Sicherheitslücken enthält, können Sie die spezielle OWASP-Liste verwenden . Sie können auch die Bibliothek verwenden, die Pakete auf bekannte Sicherheitslücken überprüft. Retire.js.

Verwenden Sie keine unsicheren Funktionen.

Es gibt Funktionen, die nach Möglichkeit verworfen werden sollten. Zu diesen Funktionen gehört eval (), das eine als Argument verwendete Zeichenfolge ausführt. In Kombination mit Benutzereingaben kann die Verwendung dieser Funktion zu einer Sicherheitsanfälligkeit bezüglich Remotecodeausführung führen, da die Verwendung von child_process.exec aus ähnlichen Gründen ebenfalls unsicher ist, da die Funktion die empfangenen Argumente an bin / sh übergibt.

Darüber hinaus gibt es eine Reihe von Modulen, die Sie mit Vorsicht verwenden sollten. Zum Beispiel das fs-Modul zum Arbeiten mit Dateien. Wenn die generierte Benutzereingabe auf bestimmte Weise an eine Funktion übergeben wird, kann Ihre Anwendung anfällig für das Einschließen einer lokalen Datei und eines Verzeichnisdurchlaufs werden.

Das VM-Modul, das eine API zum Kompilieren und Ausführen von Code auf einer virtuellen V8-Maschine bereitstellt, sollte nur in der Sandbox verwendet werden.

Hier können Sie sich mit anderen Funktionen vertraut machen, die Ihre Anwendung möglicherweise unsicher machen.

Seien Sie vorsichtig mit regulären Ausdrücken.

Ein regulärer Ausdruck kann so geschrieben werden, dass Sie eine Situation erreichen, in der der Ausdruck exponentiell wächst, was zu einem Denial-of-Service führen kann. Solche Angriffe werden als ReDoS bezeichnet. Es gibt verschiedene Tools, mit denen überprüft werden kann, ob reguläre Ausdrücke sicher sind. Eines davon ist der Vuln-Regex-Detektor.

Linter regelmäßig ausführen

Während der Entwicklung ist es schwierig, alle Empfehlungen zur Gewährleistung der Sicherheit zu berücksichtigen. Wenn es um die Teamentwicklung geht, ist es nicht einfach, die Einhaltung der Regeln durch alle Teammitglieder zu erreichen. Für solche Zwecke gibt es Tools für die statische Sicherheitsanalyse. Solche Tools suchen, ohne Ihren Code auszuführen, nach Schwachstellen darin. Darüber hinaus können Sie mit Lintern benutzerdefinierte Regeln hinzufügen, um Stellen im Code zu finden, die möglicherweise anfällig sind. Die am häufigsten verwendeten Linters sind ESLint und JSHint.

Verwenden Sie den strengen Modus.

Javascript verfügt über eine Reihe unsicherer und veralteter Funktionen, die nicht verwendet werden sollten. Um die Möglichkeit der Verwendung dieser Funktionen auszuschließen, wird auch der strikte Modus bereitgestellt.

Allgemeine Sicherheitsgrundsätze einhalten

Die beschriebenen Empfehlungen konzentrieren sich auf NodeJS, vergessen jedoch nicht die allgemeinen Sicherheitsprinzipien, die unabhängig von der verwendeten Plattform eingehalten werden müssen.

All Articles