Skip to main content

Como evitar a desserialização insegura no Node.js

blog feature playwright tests

17 de abril de 2023

0 minutos de leitura

Nota do editor: 31 de maio de 2023

Esta publicação foi atualizada para mostrar como o Snyk Open Source e a extensão Snyk para VS Code podem ajudar você a evitar a desserialização insegura em seus projetos.

Serialização é o processo de converter um objeto JavaScript em um fluxo de bytes sequenciais para enviá-lo pela rede ou salvá-lo em um banco de dados. A serialização altera o formato original dos dados, mas preserva seu estado e suas propriedades, permitindo recriá-los quando necessário. Com a serialização, podemos gravar dados complexos em arquivos, bancos de dados e memória entre processos, além de enviá-los entre componentes e pela rede.

A desserialização é o oposto da serialização: ela converte os dados serializados novamente em um objeto no mesmo estado em que estavam antes. O problema é que a desserialização pode ser insegura e expor dados confidenciais. A principal causa da desserialização insegura é não proteger o processo de desserialização de entradas fornecidas pelos usuários. A melhor forma de evitá-la é garantir que apenas dados validados e sanitizados sejam desserializados.

Os invasores podem explorar essa vulnerabilidade inserindo código malicioso em um objeto serializado aparentemente inofensivo e enviando-o para uma aplicação. Sem verificações de segurança adicionais, a aplicação web desserializa o objeto recebido e executa o código malicioso. Outra possibilidade é o invasor extrair dados confidenciais contidos em um objeto serializado.

As consequências da serialização insegura podem ser graves. Quando um site desserializa dados de forma insegura, por exemplo, hackers podem manipular objetos serializados e inserir dados nocivos no código de uma aplicação. Um invasor pode até enviar um objeto serializado completamente diferente para acessar outras partes da aplicação, deixando-a vulnerável a novos ataques e à perda de dados.

Desserialização insegura no Node.js

node-serialize e serialize-to-js são pacotes para Node.js e JavaScript vulneráveis à desserialização insegura. Ao contrário de JSON.parse e JSON.stringify, que serializam objetos apenas no formato JSON, essas duas bibliotecas serializam objetos de quase todos os tipos, inclusive funções. Essa característica as torna vulneráveis à poluição de protótipo, um ataque de injeção no qual um agente malicioso assume o controle dos valores padrão das propriedades de um objeto.

Se um ataque de poluição de protótipo for bem-sucedido, o invasor poderá causar danos adicionais ao alterar a lógica da aplicação, iniciar a execução remota de código ou provocar um ataque de negação de serviço (DoS).

Este artigo mostra como corrigir vulnerabilidades de desserialização no Node.js. Vamos criar um código vulnerável, demonstrar um ataque e, em seguida, corrigir as vulnerabilidades.

Pré-requisitos

Para acompanhar este tutorial, você precisará de:

  • Node.js instalado

  • Conhecimento de JavaScript

Como corrigir vulnerabilidades de desserialização no Node.js

Vamos começar configurando um projeto Node.js. Execute o seguinte comando:

npm init -y

Instale o framework de aplicações web Express:

npm install express

Instale o pacote de serialização do Node.js:

npm install node-serialize

Instale um parser de cookies:

npm install cookie-parser

Crie um novo arquivo server.js e adicione o código abaixo:

var express = require('express');
var cookieParser = require('cookie-parser');
var escape = require('escape-html');
var serialize = require('node-serialize');
var app = express();
app.use(cookieParser())

app.get('/', function(req, res) {
	if (req.cookies.profile) {
		var str = new Buffer(req.cookies.profile, 'base64').toString();
		var obj = serialize.unserialize(str);
		if (obj.username) {
			res.send("Hello " + escape(obj.username));
		}
	} else {
		res.cookie('profile', "eyJ1c2VybmFtZSI6IkpvaG4iLCJnZW5kZXIiOiJNYWxlIiwiQWdlIjogMzV9", {
			maxAge: 900000,
			httpOnly: true
		});
	}
	res.send("Welcome to the Serialize-Deserialize Demo!");
});
app.listen(3000);
console.log("Listening on port 3000...");

No arquivo acima, usamos o módulo inseguro node-serialize. Também passamos dados não confiáveis para a função unserialize().

Quantos problemas de segurança você consegue identificar no código acima? Como saber se há problemas de segurança no código ou nas dependências que adicionamos ao projeto?

É aqui que entra a extensão Snyk Code. Ela é gratuita e rápida de configurar. Acesse Extensões no VS Code (SHIFT+CMD+X), pesquise por Snyk, instale a extensão e junte-se a mais de 90 mil desenvolvedores.

Veja uma captura de tela do Snyk Code em ação com o trecho de código da aplicação Express anterior:

Captura de tela mostrando vários possíveis problemas de segurança no servidor de aplicativos Express.

Observe as setas roxas e como elas apontam possíveis problemas de segurança neste servidor de aplicação Express. Se você olhar com atenção, encontrará outros problemas e também verá como o Snyk Code anota as linhas de código no arquivo `server.js`, mostrando como o código inseguro percorre diferentes caminhos e arquivos de código.

Baixe a extensão Snyk para VS Code. É grátis!

Voltando à aplicação web Node.js, o resultado é que o endpoint app.get('/', function(req, res) fica vulnerável à desserialização insegura. Isso acontece porque a aplicação adiciona as entradas dos usuários ao valor de um cookie de preferências que já está serializado.

Para demonstrar a vulnerabilidade, execute o servidor Express com este comando:

node server.js

Isso inicia o servidor na porta 3000. Acesse http://localhost:3000/ para visualizar a página e inspecionar os cookies, como mostrado abaixo:

As ferramentas de desenvolvedor do navegador mostram a guia Application com um cookie localhost chamado “profile2” e seu valor codificado.

Agora, converta o cookie do formato base64 para JSON. Você pode fazer isso usando uma ferramenta online, como Code Beautify. Este é o cookie em base64:

eyJ1c2VybmFtZSI6IkpvaG4iLCJnZW5kZXIiOiJNYWxlIiwiQWdlIjogMzV9

E este é o cookie definido pelo servidor depois que o convertemos de base64 para JSON:

{"username":"John","gender":"Male","Age": 35}

Para ver o problema de desserialização em ação, vamos alterar o valor do objeto JSON e codificá-lo em base64. Depois, substitua o valor do cookie no navegador pelo novo cookie.

Altere o objeto JSON para:

{"username":"Joe Jones","gender":"Male","Age": 40}

Acesse esta ferramenta de Base64 para codificar o JSON. Depois de codificado em base64, ele fica assim:

eyJ1c2VybmFtZSI6IkpvZSBKb25lcyIsImdlbmRlciI6Ik1hbGUiLCJBZ2UiOiA0MH0=

Agora, substitua o valor atual do cookie no navegador pelo valor codificado do novo objeto e edite o valor da resposta HTTP no arquivo server.js para:

res.send("Welcome to the Insecure Deserialize Demo!");

Ao reiniciar a conexão e atualizar a página, você verá a nova resposta.

Explorando a vulnerabilidade

Vamos explorar a vulnerabilidade para executar código arbitrário (ACE), passando dados não confiáveis para a função unserialize().

Primeiro, vamos criar um payload com a função serialize() do mesmo módulo que estamos usando. Para isso, crie um arquivo chamado serialize.js e adicione o seguinte código:

var serialize = require('node-serialize');
var m = {
	myOutput: function() {
		return 'Hello';
	}
}
console.log("Serialized: \n" + serialize.serialize(m) + "\n");

Este é o script Node.js que vai serializar nosso código.

Agora, abra um terminal, navegue até a pasta raiz e execute este comando:

node serialize.js

Substitua o parâmetro username no valor do cookie pelo valor do parâmetro myOutput exibido na saída acima. O novo objeto no cookie deverá ficar assim:

{"username":"_$$ND_FUNC$$_function(){ return 'Hello'; }","gender":"Male","Age": 40}

A saída acima fornece um payload serializado para passar à função unserialize(). Para obter ACE, precisamos usar uma expressão de função imediatamente invocada (IIFE) do JavaScript para chamar a função.

Ao adicionar os parênteses de IIFE, (), depois do corpo da função neste payload serializado, a função será executada após a criação do objeto. Veja abaixo o objeto JSON que ficará armazenado no cookie.

{
	"username": "_$$ND_FUNC$$_function(){ return 'Hello'; }()",
	"gender": "Male",
	"Age": 40
};

Para demonstrar como esse objeto manipulado introduz a avaliação dinâmica de código em um servidor Node.js em execução, vamos usá-lo como exemplo no arquivo utilitário serialize.js. Vamos passar esse payload para uma função unserialize(). Modifique o arquivo seralize.js adicionando o seguinte código:

var serialize = require('node-serialize');
var m = {
	myOutput: function() {
		return 'Hello';
	}
}

console.log("Serialized: \n" + serialize.serialize(m) + "\n");

var km = {
	"username": "_$$ND_FUNC$$_function(){ return 'Hello'; }()",
	"gender": "Male",
	"Age": 40
};
console.log(serialize.unserialize(km));

No código acima, adicionamos uma nova variável, km, cujo valor é uma função atribuída a username, que passamos para a função unserialize().

Agora, execute o arquivo de script utilitário com o comando abaixo para verificar se a função é executada:

node serialize.js

Você deverá obter a seguinte saída:

Exemplo de código que mostra dados JavaScript serializados e sua saída desserializada, com campos de nome de usuário, gênero e idade.

A exploração do processo de serialização foi bem-sucedida! Como invasores que controlam a entrada do objeto no lado do cliente, conseguimos manipulá-lo para incluir código JavaScript que altera o campo username.

Como corrigir a vulnerabilidade de desserialização insegura

A melhor forma de evitar a desserialização insegura é não desserializar entradas de usuários. A segunda melhor opção é verificar as entradas dos usuários antes de serializá-las. Podemos usar um pacote chamado Serialize JavaScript para sanitizar as entradas no exemplo vulnerável acima.

Primeiro, instale serialize-javaScript usando npm:

npm install serialize-javascript

Modifique o arquivo serialize.js, substituindo o código pelo seguinte:

var serialize = require('serialize-javascript');
var m = {
	myOutput: function() {
		return 'Hello';
	}
}
console.log("Serialized: \n" + serialize(m, {
	ignoreFunction: true
}) + "\n");

A opção ignoreFunction garante que as funções usadas para executar ACE não sejam serializadas. Execute o arquivo para confirmar:

node serialize.js

Veja a saída:

Trecho de código exibindo "Serialized:" seguido de um objeto vazio, "{}".

Vamos confirmar que o novo serialize-javascript recomendado neste artigo realmente não tem vulnerabilidades, de acordo com os relatórios de CVE. No comando npm ls abaixo, você verá que ele foi adicionado ao projeto junto com o pacote vulnerável node-serialize:

$ npm ls
b@1.0.0 /private/tmp/b
├── cookie-parser@1.4.6
├── express@4.18.2
├── node-serialize@0.0.4
└── serialize-javascript@6.0.1

Em seguida, fazemos uma varredura com o pacote npm gratuito snyk:

$ snyk test

Testing /private/tmp/b...

Tested 62 dependencies for known issues, found 1 issue, 1 vulnerable path.

Issues with no direct upgrade or patch:
  ✗ Arbitrary Code Execution [Critical Severity][https://security.snyk.io/vuln/npm:node-serialize:20170208] in node-serialize@0.0.4
    introduced by node-serialize@0.0.4
  No upgrade or patch available

Organization:      lirantal
Package manager:   npm
Target file:       package-lock.json

Isso mostra que o pacote npm node-serialize é vulnerável, como demonstramos neste artigo, e que não há vulnerabilidades de segurança conhecidas publicamente no pacote npm serialize-javascript recomendado.

Recomendamos que você comece a usar o Snyk Open Source e inclua o pacote npm snyk no seu fluxo de trabalho de desenvolvimento. Não se esqueça de adicioná-lo ao pipeline de CI/CD para evitar que novas vulnerabilidades de segurança passem despercebidas.

Boas práticas para evitar vulnerabilidades de desserialização

Desserializar entradas de usuários pode permitir que agentes maliciosos ataquem um sistema e exponham dados confidenciais. A melhor forma de se proteger contra a desserialização insegura é evitar desserializar dados de fontes não confiáveis. Mas, se precisarmos desserializar dados, devemos implementar medidas de segurança adicionais para verificar se eles não foram manipulados.

Além disso, devemos sanitizar as entradas. Podemos fazer isso verificando se o objeto a ser serializado contém funções e interrompendo a serialização caso contenha.

Desserialização segura

Serialização e desserialização são processos essenciais que permitem transferir dados pela rede sem interrupções. No entanto, a desserialização insegura pode levar a vulnerabilidades críticas. Ao inserir objetos serializados maliciosos em uma aplicação web — geralmente na forma de dados fornecidos pelo usuário que são posteriormente desserializados —, os invasores podem passar dados nocivos para a aplicação. Em seguida, podem iniciar um ataque de injeção, executar código remotamente ou realizar um ataque DDoS.

Podemos evitar ataques causados por essa vulnerabilidade simplesmente não desserializando entradas de usuários. Se a desserialização for necessária, devemos incluir mecanismos de segurança adicionais, como tokens antifalsificação, para garantir que os dados não tenham sido alterados.

Acesse o blog da Snyk para saber mais sobre como evitar vulnerabilidades de serialização e desserialização inseguras em Java.

Para proteger seu código contra vulnerabilidades de serialização e desserialização inseguras, adicione o plugin da Snyk.io à sua IDE.

Comece a jogar Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo à gravação sob demanda do nosso workshop virtual introdutório.