Hojas de trucos de seguridad: Nodejs



Ya se ha dicho mucho sobre la popularidad de NodeJS. El aumento en el número de aplicaciones es obvio: NodeJS es bastante fácil de aprender, tiene una gran cantidad de bibliotecas y un ecosistema en desarrollo dinámico.

Hemos preparado recomendaciones para desarrolladores de NodeJS basadas en OWASP Cheat Sheets para ayudarlo a anticipar problemas de seguridad al desarrollar aplicaciones.

Las recomendaciones de seguridad para las aplicaciones NodeJS se pueden dividir en las siguientes categorías:

  • Seguridad durante el desarrollo de la aplicación;
  • Seguridad del servidor;
  • Seguridad de la plataforma;


Seguridad de desarrollo de aplicaciones


Evite el infierno de devolución de llamada El

uso de funciones de devolución de llamada (devoluciones de llamada) es una de las mayores fortalezas de NodeJS, sin embargo, al anidar devoluciones de llamada, puede olvidarse fácilmente de manejar el error en una de las funciones. Una forma de evitar el infierno de devolución de llamada es usar promesas. Incluso si el módulo que está utilizando no admite trabajar con promesas, siempre puede usar Promise.promisifyAll (). Pero incluso usando promesas, vale la pena prestar atención a la anidación. Para evitar completamente el error del infierno de devolución de llamada, adhiérase a una cadena de promesas "planas".

Ejemplo de infierno de devolución de llamada:

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

Mismo código usando promesas de cadena plana:

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

Limite el tamaño de la solicitud.

Analizar el cuerpo de la solicitud puede ser una operación que requiere muchos recursos. Si no limita el tamaño de la solicitud, los atacantes podrán enviar solicitudes lo suficientemente grandes que pueden llenar todo el espacio en disco o agotar todos los recursos del servidor, pero al mismo tiempo, limitar el tamaño de la solicitud para todos los casos puede ser incorrecto, porque hay solicitudes, como descargar un archivo. Por lo tanto, se recomienda establecer límites para diferentes tipos de contenido. Por ejemplo, usando el marco express, esto se puede implementar de la siguiente manera:

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

Cabe señalar que un atacante puede cambiar el tipo de contenido de la solicitud y eludir las restricciones, por lo tanto, es necesario verificar si el contenido de la solicitud coincide con el tipo de contenido especificado en el encabezado de la solicitud. Si verificar el tipo de contenido afecta el rendimiento, solo puede verificar ciertos tipos o consultas que son más grandes que un cierto tamaño.

No bloquee el bucle de eventos

Un componente importante del lenguaje es el bucle de eventos, que solo le permite cambiar el contexto de ejecución sin esperar a que se complete la operación. Sin embargo, hay operaciones de bloqueo cuya finalización NodeJS tiene que esperar antes de continuar con el código. Por ejemplo, la mayoría de los métodos sincrónicos están bloqueando:

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

Se recomienda realizar tales operaciones de forma asincrónica:

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

Al mismo tiempo, no olvide que el código que se encuentra después de la llamada asincrónica se ejecutará sin esperar la finalización de la operación anterior.

Por ejemplo, en el siguiente código, el archivo se eliminará antes de leerlo, lo que puede provocar una condición de carrera.

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

Para evitar esto, puede escribir todas las operaciones en una función sin bloqueo:

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

Verificar campos de entrada

Verificar los campos de entrada es una parte importante de la seguridad de cualquier aplicación. Los errores de validación pueden hacer que su aplicación se vuelva vulnerable de inmediato a muchos tipos de ataques: inyección sql, xss, inyección de comandos y otros. Para simplificar la validación de formularios, puede usar paquetes de validación, mongo-express-sanitize.

Escape de los datos del usuario

Una de las reglas que lo ayudará a protegerse de los ataques xss es proteger los datos del usuario. Puede usar la biblioteca escape-html o node-esapi para esto.

Mantener registros

Además, ayudará en la depuración de errores, el registro se puede utilizar para responder a incidentes. Puede leer más sobre la necesidad de iniciar sesión aquí.. Uno de los paquetes de registro de NodeJS más populares es Winston y Bunyan. El siguiente ejemplo muestra cómo usar Winston para generar registros tanto en la consola como en el archivo:

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

Controle el ciclo de eventos

Si su servidor se encuentra en condiciones de tráfico intenso de red, los usuarios pueden experimentar dificultades con la disponibilidad de su servicio. Esto es esencialmente un ataque DoS. En este caso, puede rastrear el tiempo de respuesta y, si excede el tiempo especificado, enviar un mensaje a 503 Server Too Busy. El módulo toobusy-js puede ayudar.

Un ejemplo de uso del módulo:

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();
    }
});

Tome precauciones contra la fuerza bruta.

Nuevamente, los módulos vienen al rescate. Por ejemplo, express-brute o express-bouncer. Ejemplo de uso:

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

Usar CAPTCHA es otra contramedida de fuerza bruta común. Un módulo de uso frecuente para ayudar a implementar CAPTCHA es svg-captcha.

Usar tokens CSRF

Una de las formas más confiables para protegerse contra los ataques CSRF es usar un token CSRF. El token debe ser generado con alta entropía, estrictamente verificado y estar vinculado a la sesión del usuario. Para garantizar el funcionamiento del token CSRF, puede utilizar el módulo csurf.

Ejemplo de uso:

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');
});

No olvide agregar el token al campo oculto en la página:

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

Puede leer más sobre los tokens CSRF en nuestro artículo .

Elimine rutas innecesarias. La

aplicación web no debe contener páginas que los usuarios no usen, ya que esto puede aumentar la superficie de ataque. Por lo tanto, todas las rutas API no utilizadas deben estar deshabilitadas. Debería prestar especial atención a esta pregunta si utiliza marcos de Sails o Feathers, ya que generan automáticamente puntos finales API.

Protéjase de la HPP (contaminación de parámetros HTTP)

De forma predeterminada, express agrega todos los parámetros de la solicitud a una matriz. OWASP recomienda utilizar el módulo hpp, que ignora todos los valores de los parámetros de req.query y / o req.body y simplemente selecciona el último valor entre los duplicados.

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

Supervise los valores devueltos.

Por ejemplo, la tabla de usuario puede almacenar datos importantes: contraseña, dirección de correo electrónico, fecha de nacimiento, etc. Por lo tanto, es importante devolver solo los datos necesarios.

Por ejemplo:

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

Usar descriptores

Use descriptores para describir el comportamiento de una propiedad para varias operaciones: de escritura, si es posible cambiar el valor de una propiedad, enumerable, si es posible usar una propiedad en un bucle for..in, configurable, si es posible sobrescribir una propiedad. Se recomienda prestar atención a las propiedades enumeradas, ya que al definir la propiedad de un objeto, todos estos atributos se establecen en verdadero de forma predeterminada. Puede cambiar el valor de las propiedades de la siguiente manera:

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

Utilice ACL

Acl puede ayudar a diferenciar el acceso a datos en función de los roles. Por ejemplo, agregar permisos se ve así:

// 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'])

Coger uncaughtException

Por defecto, en el caso de una excepción no capturada, NodeJS tirarán el seguimiento de pila actual y terminar el hilo de ejecución. Sin embargo, NodeJS le permite personalizar este comportamiento. En el caso de una excepción no detectada, se genera un evento uncaughtException, que se puede detectar utilizando el objeto de proceso:

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

Vale la pena recordar que cuando se produce una excepción no capturada, es necesario borrar todos los recursos asignados (por ejemplo, descriptores de archivo y controladores) antes de completar el proceso Z para evitar errores imprevistos. Se desaconseja encarecidamente que el programa continúe ejecutándose si se produce una excepción no capturada.

Además, al mostrar mensajes de error, el usuario no debe revelar información de error detallada, como el seguimiento de la pila.

Seguridad del servidor


Establezca indicadores para los encabezados cuando trabaje con cookies.

Existen varios indicadores que pueden ayudar a proteger contra ataques como xss y csrf: httpOnly, que impide el acceso a las cookies a través de javascript; Seguro: permite enviar cookies solo a través de HTTPS y SameSite, lo que determina la capacidad de transferir cookies a un recurso de terceros.

Ejemplo de uso:

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

Establecer encabezados HTTP para seguridad

Los siguientes son encabezados y ejemplos de cómo conectarlos para ayudarlo a protegerse de una serie de ataques comunes. Los encabezados se configuran utilizando el módulo de casco

• Strict-Transport-Security: HTTP Strict Transport Security (HSTS) le dice al navegador que solo se puede acceder a la aplicación a través de HTTPS

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

• Opciones de marco X: determina si la página se puede usar en marco, marco flotante, incrustación u objeto

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

• Protección X-XSS: permite que el navegador deje de cargar la página si detecta un ataque XSS reflejado.

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

• Opciones de tipo de contenido X: se utilizan para evitar ataques con tipos MIME

app.use(helmet.noSniff());

• Política de seguridad de contenido: evita ataques como XSS y ataques de inyección de datos

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 y Pragma: para administrar el almacenamiento en caché, especialmente este encabezado puede ser útil para páginas que contienen datos confidenciales. Sin embargo, recuerde que deshabilitar el almacenamiento en caché en todas las páginas puede afectar el rendimiento.

app.use(helmet.noCache());

• X-Download-Options: el encabezado evita que Inter Explorer ejecute archivos descargados

app.use(helmet.ieNoOpen());

• Expect-CT: Transparencia de certificados: un mecanismo creado para resolver algunos problemas con la infraestructura de certificados SSL, este encabezado le dice al navegador la necesidad de una verificación de certificado adicional en los registros de CT

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: un encabezado opcional que se utiliza para indicar la tecnología utilizada en el servidor. Puede ocultar este encabezado de la siguiente manera:

app.use(helmet.hidePoweredBy());

Además, puede cambiar el valor para ocultar información real sobre las tecnologías que utiliza:

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

Seguridad de la plataforma


Actualice sus paquetes La

seguridad de su aplicación depende de la seguridad de los paquetes que utiliza, por lo que es importante utilizar la última versión del paquete. Para asegurarse de que el paquete que está utilizando no contenga vulnerabilidades conocidas, puede usar la lista especial OWASP . También puede usar la biblioteca que comprueba los paquetes en busca de vulnerabilidades conocidas Retire.js.

No use funciones inseguras.

Hay funciones que se recomienda descartar siempre que sea posible. Entre estas funciones está eval (), que ejecuta una cadena tomada como argumento. En combinación con la entrada del usuario, el uso de esta función puede generar vulnerabilidades en la ejecución remota de código, ya que por razones similares, el uso de child_process.exec tampoco es seguro, porque la función pasa los argumentos recibidos a bin / sh.

Además, hay una serie de módulos que debe usar con precaución. Por ejemplo, el módulo fs para trabajar con archivos. Si de alguna manera la entrada del usuario generada se pasa a una función, entonces su aplicación puede volverse vulnerable a incluir un archivo local y un recorrido de directorio.

El módulo vm, que proporciona una API para compilar y ejecutar código en una máquina virtual V8, solo debe usarse en el entorno limitado.

Aquí puede familiarizarse con otras funciones que pueden hacer que su aplicación sea insegura.

Tenga cuidado al usar expresiones regulares.

Se puede escribir una expresión regular para que pueda lograr una situación en la que la expresión crezca exponencialmente, lo que puede conducir a una denegación de servicio. Tales ataques se llaman ReDoS. Hay varias herramientas para verificar si las expresiones regulares son seguras, una de las cuales es vuln-regex-detector.

Ejecute linter periódicamente

Durante el desarrollo, es difícil tener en cuenta todas las recomendaciones para garantizar la seguridad, y si se trata del desarrollo del equipo, no es fácil lograr el cumplimiento de las reglas por parte de todos los miembros del equipo. Para tales fines, existen herramientas para el análisis de seguridad estático. Dichas herramientas, sin ejecutar su código, buscan vulnerabilidades en él. Además, las listas le permiten agregar reglas personalizadas para encontrar lugares en el código que pueden ser vulnerables. Las linters más utilizadas son ESLint y JSHint.

Utilice el modo estricto.

Javascript tiene una serie de funciones inseguras y obsoletas que no deben utilizarse. Para excluir la posibilidad de usar estas funciones, también se proporciona el modo estricto.

Adherirse a los principios generales de seguridad.

Las recomendaciones descritas se centran en NodeJS, pero no se olvide de los principios generales de seguridad que deben observarse independientemente de la plataforma utilizada.

All Articles