Folhas de dicas de segurança: Nodejs



Muito já foi dito sobre a popularidade do NodeJS. O aumento no número de aplicativos é óbvio - o NodeJS é bastante fácil de aprender, possui um grande número de bibliotecas e um ecossistema em desenvolvimento dinâmico.

Fizemos recomendações para desenvolvedores do NodeJS com base nas folhas de dicas do OWASP para ajudá-lo a antecipar problemas de segurança ao desenvolver aplicativos.

As recomendações de segurança para aplicativos NodeJS podem ser divididas nas seguintes categorias:

  • Segurança durante o desenvolvimento de aplicativos;
  • Segurança do servidor;
  • Segurança da plataforma;


Segurança no desenvolvimento de aplicativos


Evite o inferno de retorno de chamada O

uso de funções de retorno de chamada (retornos de chamada) é um dos pontos fortes do NodeJS; no entanto, ao aninhar retornos de chamada, você pode esquecer facilmente de lidar com o erro em uma das funções. Uma maneira de evitar o inferno de retorno de chamada é usar promessas. Mesmo que o módulo que você está usando não suporte o trabalho com promessas, você sempre pode usar Promise.promisifyAll (). Mas, mesmo usando promessas, vale a pena prestar atenção ao aninhamento. Para evitar completamente o erro de retorno de chamada, atenha-se a uma cadeia de promessas "plana".

Exemplo infernal de retorno de chamada:

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

Mesmo código usando promessas de cadeia 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
   });

Limitar o tamanho da solicitação A

análise do corpo da solicitação pode ser uma operação que consome muitos recursos. Se você não limitar o tamanho da solicitação, os invasores poderão enviar solicitações grandes o suficiente para ocupar todo o espaço em disco ou esgotar todos os recursos do servidor, mas, ao mesmo tempo, limitar o tamanho da solicitação para todos os casos pode estar incorreto, pois há solicitações, como o download de um arquivo. Portanto, é recomendável definir limites para diferentes tipos de conteúdo. Por exemplo, usando a estrutura expressa, isso pode ser implementado da seguinte maneira:

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

Deve-se observar que um invasor pode alterar o tipo de conteúdo da solicitação e contornar as restrições; portanto, é necessário verificar se o conteúdo da solicitação corresponde ao tipo de conteúdo especificado no cabeçalho da solicitação. Se a verificação do tipo de conteúdo afetar o desempenho, você poderá verificar apenas determinados tipos ou consultas maiores que um determinado tamanho.

Não bloqueie o loop de eventos

Um componente importante da linguagem é o loop de eventos, que permite alternar o contexto de execução sem aguardar a conclusão da operação. No entanto, existem operações de bloqueio cuja conclusão o NodeJS precisa aguardar antes de continuar com o código. Por exemplo, a maioria dos métodos síncronos está bloqueando:

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

É recomendável executar essas operações de forma assíncrona:

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

Ao mesmo tempo, não esqueça que o código parado após a chamada assíncrona será executado sem aguardar a conclusão da operação anterior.

Por exemplo, no código abaixo, o arquivo será excluído antes de ser lido, o que pode levar a uma condição de corrida.

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

Para evitar isso, você pode gravar todas as operações em uma função sem bloqueio:

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 A

verificação dos campos de entrada é uma parte importante da segurança de qualquer aplicativo. Os erros de validação podem tornar seu aplicativo vulnerável imediatamente a muitos tipos de ataques: injeção de sql, xss, injeção de comando e outros. Para simplificar a validação de formulário, você pode usar pacotes validadores, mongo-express-sanitize.

Escapar dos dados do usuário

Uma das regras que ajudarão você a se proteger de ataques xss é proteger os dados do usuário. Você pode usar a biblioteca escape-html ou node-esapi para isso.

Manter registros

Além disso, ele ajudará em erros de depuração, o registro pode ser usado para responder a incidentes. Você pode ler mais sobre a necessidade de registro aqui.. Um dos pacotes de registro mais populares do NodeJS é o Winston e o Bunyan. O exemplo abaixo mostra como usar o Winston para gerar logs para o console e o arquivo:

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

Controlar o ciclo de eventos

Se o seu servidor estiver em condições de intenso tráfego de rede, os usuários poderão ter dificuldades com a disponibilidade do seu serviço. Este é essencialmente um ataque de negação de serviço. Nesse caso, você pode rastrear o tempo de resposta e, se exceder o tempo especificado, enviar uma mensagem para 503 Server Too Busy. O módulo toobusy-js pode ajudar.

Um exemplo de uso do 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 precauções contra a força bruta.Mais uma

vez, os módulos são úteis. Por exemplo, express-brute ou express-bouncer. Exemplo 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 );
    }
});

O uso do CAPTCHA é outra contramedida comum de força bruta. Um módulo usado com freqüência para ajudar a implementar o CAPTCHA é o svg-captcha.

Use tokens CSRF

Uma das maneiras mais confiáveis ​​de se proteger contra ataques CSRF é usar um token CSRF. O token deve ser gerado com alta entropia, estritamente verificado e vinculado à sessão do usuário. Para garantir a operação do token CSRF, você pode usar o módulo csurf.

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

Não esqueça de adicionar o token ao campo oculto da página:

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

Você pode ler mais sobre os tokens CSRF em nosso artigo .

Excluir rotas desnecessárias.O

aplicativo Web não deve conter páginas que não são usadas pelos usuários, pois isso pode aumentar a superfície de ataque. Portanto, todas as rotas de API não utilizadas devem estar desabilitadas. Você deve prestar especial atenção a esta pergunta se usar estruturas Sails ou Feathers, pois elas geram automaticamente pontos finais da API.

Proteja-se da HPP (Poluição de Parâmetros HTTP)

Por padrão, o express adiciona todos os parâmetros da solicitação a uma matriz. A OWASP recomenda o uso do módulo hpp, que ignora todos os valores de parâmetros de req.query e / ou req.body e simplesmente seleciona o último valor entre os duplicados.

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

Monitore os valores retornados.Por

exemplo, a tabela do usuário pode armazenar dados importantes: senha, endereço de email, data de nascimento etc. Portanto, é importante retornar apenas os dados necessários.

Por exemplo:

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

Usar descritores

Use descritores para descrever o comportamento de uma propriedade para várias operações: gravável - se é possível alterar o valor de uma propriedade, enumerável - se é possível usar uma propriedade em um loop for..in, configurável - se é possível substituir uma propriedade. É recomendável prestar atenção às propriedades listadas, pois ao definir a propriedade de um objeto, todos esses atributos são verdadeiros por padrão. Você pode alterar o valor das propriedades da seguinte maneira:

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

Usar ACLs O

Acl pode ajudar a diferenciar o acesso a dados com base em funções. Por exemplo, adicionar permissão se parece com isso:

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

Capturar uncaughtException

Por padrão, no caso de uma exceção não capturada, o NodeJS lançará o rastreamento de pilha atual e encerrará o encadeamento de execução. No entanto, o NodeJS permite personalizar esse comportamento. No caso de uma exceção não capturada, um evento uncaughtException é gerado, que pode ser capturado usando o objeto de processo:

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 lembrar que, quando ocorre uma exceção não capturada, é necessário limpar todos os recursos alocados (por exemplo, descritores de arquivo e manipuladores) antes de concluir o processo Z para evitar erros imprevistos. É altamente desencorajado que o programa continue sendo executado se ocorrer uma exceção não detectada.

Além disso, ao exibir mensagens de erro, o usuário não deve divulgar informações detalhadas sobre erros, como rastreamento de pilha.

Segurança do servidor


Definir sinalizadores para cabeçalhos ao trabalhar com cookies.Existem

vários sinalizadores que podem ajudar a proteger contra ataques como xss e csrf: httpOnly, que impede o acesso a cookies através de javascript; Seguro - permite o envio de cookies apenas via HTTPS e SameSite, que determina a capacidade de transferir cookies para um recurso de terceiros.

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

Definir cabeçalhos HTTP para segurança

A seguir, estão os cabeçalhos e exemplos de como conectá-los para ajudá-lo a se proteger de uma série de ataques comuns. Os cabeçalhos são definidos usando o módulo capacete

• Segurança de transporte restrita: HSTS (HTTP Strict Transport Security) informa ao navegador que o aplicativo pode ser acessado apenas via HTTPS

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

• Opções de quadro X: determina se a página pode ser usada em quadro, iframe, incorporação ou objeto

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

• Proteção X-XSS: permite que o navegador pare de carregar a página se detectar um ataque XSS refletido.

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

• Opções de tipo de conteúdo X: usadas para impedir ataques usando tipos MIME

app.use(helmet.noSniff());

• Política de segurança de conteúdo: evita ataques como XSS e ataques de injeção de dados

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'"]
    }
}))

• Controle de cache e Pragma: para gerenciar o cache, especialmente esse cabeçalho pode ser útil para páginas que contêm dados confidenciais. No entanto, lembre-se de que desativar o cache em todas as páginas pode afetar o desempenho.

app.use(helmet.noCache());

• Opções de download X: cabeçalho impede que o Inter Explorer execute arquivos baixados

app.use(helmet.ieNoOpen());

• Expect-CT: Transparência de certificado - um mecanismo criado para solucionar alguns problemas com a infraestrutura de certificado SSL, este cabeçalho informa ao navegador sobre a necessidade de verificação adicional de certificado nos logs 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: um cabeçalho opcional usado para indicar a tecnologia usada no servidor. Você pode ocultar este cabeçalho da seguinte maneira:

app.use(helmet.hidePoweredBy());

Além disso, você pode alterar o valor para ocultar informações reais sobre as tecnologias que você usa:

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

Segurança da plataforma


Atualize seus pacotes A

segurança do seu aplicativo depende da segurança dos pacotes que você usa, por isso é importante usar a versão mais recente do pacote. Para garantir que o pacote que você está usando não contenha vulnerabilidades conhecidas, você pode usar a lista especial do OWASP . Você também pode usar a biblioteca que verifica os pacotes quanto a vulnerabilidades conhecidas Retire.js.

Não use funções inseguras.

Há funções que devem ser descartadas sempre que possível. Entre essas funções está eval (), que executa uma string tomada como argumento. Em combinação com a entrada do usuário, o uso dessa função pode levar a vulnerabilidades na execução remota de código, pois, por razões semelhantes, o uso de child_process.exec também é inseguro, porque a função passa os argumentos recebidos para bin / sh.

Além disso, há vários módulos que você deve usar com cuidado. Por exemplo, o módulo fs para trabalhar com arquivos. Se, de certa maneira, a entrada do usuário gerada for passada para uma função, seu aplicativo poderá ficar vulnerável à inclusão de um arquivo local e de passagem de diretório.

O módulo vm, que fornece uma API para compilar e executar código em uma máquina virtual V8, deve ser usado apenas na sandbox.

Aqui você pode se familiarizar com outras funções que podem tornar seu aplicativo inseguro.Tenha

cuidado com expressões regulares.Uma expressão

regular pode ser escrita para que você possa alcançar uma situação em que a expressão crescerá exponencialmente, o que pode levar a uma negação de serviço. Esses ataques são chamados de ReDoS. Existem várias ferramentas para verificar se as expressões regulares são seguras, uma das quais é o vuln-regex-detector.

Execute o linter periodicamente

Durante o desenvolvimento, é difícil manter todas as recomendações de segurança em mente e, quando se trata de desenvolvimento da equipe, não é fácil alcançar a conformidade com as regras de todos os membros da equipe. Para tais fins, existem ferramentas para análise de segurança estática. Essas ferramentas, sem executar seu código, procuram vulnerabilidades nele. Além disso, os linters permitem adicionar regras personalizadas para localizar lugares no código que possam estar vulneráveis. Os linters mais usados ​​são ESLint e JSHint.

Use o modo

estrito.O Javascript possui várias funções inseguras e obsoletas que não devem ser usadas. Para excluir a possibilidade de usar essas funções, o modo estrito também é fornecido.

Siga os princípios gerais de segurança

As recomendações descritas se concentram no NodeJS, mas não se esqueça dos princípios gerais de segurança que devem ser observados, independentemente da plataforma usada.

All Articles