Como proteger suas aplicações Next.js: práticas essenciais de segurança

Como proteger suas aplicações Next.js: práticas essenciais de segurança
Patrícia Silva
Patrícia Silva

Compartilhe

Imagine-se como uma pessoa exploradora em uma jornada digital, pronto para desvendar os segredos de uma ilha desconhecida.

Nessa jornada, você se depara com um mapa intrigante, representando os emaranhados caminhos de uma aplicação web.

Cada peça desse mapa corresponde a uma parte vital da aplicação, como se fossem tesouros escondidos esperando para serem descobertos.

No entanto, assim como em uma busca por um tesouro, há perigos ocultos que precisamos enfrentar.

O objetivo desse mapa do tesouro digital é apontar esses perigos, destacar áreas de atenção crítica, explicar as medidas de segurança já implementadas e oferecer dicas valiosas para conduzir auditorias nas aplicações.

Nosso principal foco é identificar e mitigar os riscos que podem comprometer a segurança e privacidade dos dados dos usuários.

Riscos à segurança e à privacidade dos dados

Agora, mergulhando mais fundo na aventura, encontramos uma série de desafios que precisamos superar para garantir a integridade e segurança da aplicação, como:

  • Exposição de chaves secretas: semelhante a deixar uma chave importante desprotegida e exposta, a exposição de chaves secretas pode abrir as portas para que invasores maliciosos acessem informações confidenciais.
  • SQL Injection e outros ataques ao banco de dados: como abrir as portas de nossa fortaleza para invasores, a falta de proteção adequada pode resultar na inserção de códigos maliciosos que comprometem a integridade dos dados armazenados.
  • Problemas de CORS (Cross-Origin Resource Sharing): como estabelecer uma linha de defesa frágil, uma configuração incorreta pode permitir que agentes externos acessem recursos sensíveis da aplicação, representando uma ameaça à segurança.
  • Vazamento de dados sensíveis: comparável a compartilhar segredos em um ambiente público, a exposição inadvertida de dados sensíveis pode levar à violação da privacidade dos usuários e consequências prejudiciais para a reputação da aplicação.
  • Problemas de autenticação e autorização: semelhante a ter portões desprotegidos em nossa fortaleza, falhas na autenticação e autorização podem permitir acesso não autorizado a áreas restritas da aplicação, comprometendo a segurança dos dados.
Banner promocional da Alura, com um design futurista em tons de azul, apresentando o texto

Como mitigar os riscos à segurança e à privacidade dos dados?

E como nós podemos nos proteger disso?

Para enfrentar esses desafios e proteger nossos tesouros digitais, contamos com o auxílio do Next.js 14 e seus poderosos aliados, os React Server Components (RSC).

Essas ferramentas funcionam como guardiões vigilantes, fortalecendo nossas defesas e garantindo a segurança de nossa aplicação:

  • Renderização no lado do servidor (Server-Side Rendering, SSR): ao renderizar páginas no servidor, antes de enviá-las ao navegador, reduzimos a exposição de dados sensíveis e minimizamos os riscos de ataques.
  • Menos código no cliente: com os React Server Components, mantemos a lógica de negócios no lado do servidor, reduzindo a complexidade e minimizando as vulnerabilidades no lado do cliente.
  • Segurança de dados sensíveis: ao operar no lado do servidor, os React Server Components protegem dados sensíveis, como chaves de API e credenciais de banco de dados, evitando sua exposição ao navegador do usuário.
  • Redução de exposição a ataques XSS: com menos código executado no cliente, mitigamos os riscos de ataques de Cross-Site Scripting (XSS), garantindo a segurança e integridade de nossa aplicação.

Com essas medidas de segurança robustas, estamos preparados para enfrentar os desafios do mundo digital e proteger nossos tesouros mais preciosos.

Dicas gerais

Dica 1: Implemente validação e sanitização de entrada

Eu diria que é obrigatório validar e sanitizar as entradas dos usuários, pois a validação checa se os dados estão no formato correto, como verificar se um input é realmente um email.

A sanitização limpa os dados para evitar scripts maliciosos, como código JavaScript inserido em um campo de texto.

Isso ajuda a proteger sua aplicação contra ataques como injeção de SQL e XSS. Um exemplo prático seria:

import { validationResult, body } from 'express-validator';

app.post('/comentario', [
  body('texto').trim().escape(),
], (req, res) => {
  // Verifica se há erros de validação na entrada
  const erros = validationResult(req);
  if (!erros.isEmpty()) {
    return res.status(400).json({ erros: erros.array() });
  }

  // Lógica para adicionar o comentário
});

Basicamente, estamos usando o express-validator, o texto do comentário é tanto trimado (removendo espaços extras) quanto escapado (transformando caracteres especiais) para garantir que a entrada seja segura para processamento, dessa forma, podemos ficar descansados.

Dica 2: Proteja dados sensíveis com criptografia

Dados sensíveis como senhas, chaves de api, chaves secretas, etc, precisam ser criptografados, e podemos usar vários métodos para criptografar senhas ou dados sensíveis.

Por exemplo: senhas, a prática recomendada é usar um algoritmo de hash seguro como por exemplo o bcrypt.

Quando um usuário cria uma senha, o bcrypt gera um hash da senha, que é o que você armazena no banco de dados.

Quando o usuário faz login, o bcrypt compara o hash da senha digitada com o hash armazenado.

const bcrypt = require('bcrypt');
const saltRounds = 10;

// Criando o hash da senha
const hashedPassword = bcrypt.hashSync(senha, saltRounds);

Para outros dados sensíveis, como informações pessoais, você pode usar bibliotecas de criptografia como crypto no Node.js.

Essas bibliotecas permitem criptografar e descriptografar dados usando chaves secretas.

const crypto = require('crypto');
const algoritmo = 'aes-256-cbc';
const chaveSecreta = 'chave-secreta'; // Deve ser mantida segura
const iv = crypto.randomBytes(16); // Vetor de inicialização

// Função para criptografar
function criptografar(texto) {
  const cipher = crypto.createCipheriv(algoritmo, chaveSecreta, iv);
  let encrypted = cipher.update(texto);
  encrypted = Buffer.concat([encrypted, cipher.final()]);
  return { iv: iv.toString('hex'), encryptedData: encrypted.toString('hex') };
}

Vale ressaltar, use HTTPS em sua aplicação para garantir que todos os dados transmitidos entre o cliente e o servidor sejam criptografados. Isso protege contra interceptações e ataques de "man-in-the-middle".

Dica 3: Implemente cabeçalhos de segurança HTTP em Next.js

Os cabeçalhos de segurança HTTP são essenciais para aumentar a segurança de aplicações Next.js, protegendo-as de perigos frequentes na internet, como os ataques de scripts entre sites (XSS).

Com a implementação adequada, esses cabeçalhos orientam como o navegador deve agir e reduzem as chances de ataques.

Preste bem atenção aos cabeçalhos HTTP importantíssimos para a segurança das aplicações:

  • X-Content-Type-Options: este cabeçalho bloqueia a interpretação automática do tipo de conteúdo pelo navegador, obrigando-o a seguir o tipo especificado. Isso é vital para prevenir a execução de scripts causada pela manipulação de tipos MIME.
  • Strict-Transport-Security (HSTS): utilizando o cabeçalho HSTS, você assegura que o navegador faça conexões apenas através de HTTPS com sua aplicação, encriptando os dados na rede e protegendo-o contra interceptações.
  • Content-Security-Policy (CSP): este cabeçalho reforça a segurança, restringindo as origens de onde scripts e estilos podem ser carregados. É uma forma eficaz contra ataques como XSS, pois limita de onde os scripts podem ser baixados.

Apenas adicione esses cabeçalhos no arquivo next.config.js da sua aplicação Next.js. Isso assegura que todas as respostas do servidor venham com os cabeçalhos de segurança estabelecidos.

Dica 4: Atualize regularmente dependências e bibliotecas

Atualizar regularmente as dependências e bibliotecas em aplicações Next.js é extremamente importante para segurança.

É o tipo de coisa que a equipe de desenvolvedores vai deixando passar, mas ao longo do tempo, isso se torna uma pedra no sapato, que machuca.

Atualizar as dependências, garante a correção de vulnerabilidades recentes e protege contra as mais novas falhas de segurança.

Novas versões também trazem melhorias de segurança e recursos atualizados, alinhando-se com os padrões de segurança mais recentes.

Incluem proteção contra ataques zero-day e contribuem para a estabilidade do sistema, reduzindo as chances de exploração por agentes maliciosos.

Cuidado ao escolher a forma de lidar com a camada de dados

Temos que ter cuidado ao escolher a abordagem de transferência de dados entre o cliente e o servidor, pois a escolha mais apropriada facilita implementar a camada de segurança.

Por exemplo, quando estamos começando um projeto novo é recomendado criar uma camada só para lidar com transferências de dados entre o cliente e o servidor.

Vamos para um exemplo: imagine que estamos criando um sistema para uma biblioteca, e neste sistema, todos os acessos aos dados dos livros, empréstimos e usuários são gerenciados por uma camada especial de acesso aos dados.

import { cache } from 'react';
import { cookies } from 'next/headers';
// libraryAuth.ts
export const getCurrentLibraryUser = cache(async () => {
  const cardNumber = cookies().get('LIBRARY_CARD');
  const validatedUser = await validateLibraryCard(cardNumber);
  // Não inclua informações secretas ou privadas como campos públicos.
  // Use classes para evitar passar o objeto inteiro para o cliente.
  return new LibraryUser(validatedUser.id);
});
import 'server-only';
import { getCurrentLibraryUser } from './libraryAuth';

function canSeeBookCheckoutHistory(viewer: LibraryUser, bookId: string) {
  // Regras de privacidade
  return viewer.checkedOutBooks.includes(bookId);
}

export async function getBookDTO(bookId: string) {
  // Não passe valores, leia os valores armazenados em cache, resolve também o contexto e torna mais fácil torná-lo lazy

  // use uma API de banco de dados que suporte a formatação segura de consultas
  const [rows] = await sql`SELECT * FROM books WHERE id = ${bookId}`;
  const bookData = rows[0];

  const currentLibraryUser = await getCurrentLibraryUser();

  // retorne apenas os dados relevantes para esta consulta e não tudo
  return {
    title: bookData.title,
    checkoutHistory: canSeeBookCheckoutHistory(currentLibraryUser, bookData.id)
      ? bookData.checkoutHistory : null,
  };
}

Os métodos que estamos falando criam objetos especiais, chamados Objetos de Transferência de Dados (DTO), que são seguros para enviar diretamente para o cliente, ou seja, o usuário final.

Esses DTOs são como pacotes prontos e seguros que contêm as informações necessárias e já estão formatados da maneira certa para o cliente usar.

Ter uma camada de dados dedicada, como no exemplo acima, centraliza o controle de acesso, melhorando a segurança e a consistência.

Esses objetos são usados apenas nos componentes que rodam no servidor, não diretamente na interface que o usuário vê. Isso é bom porque ajuda a manter a segurança.

Como os auditores de segurança precisam se preocupar mais com a parte do servidor onde os dados são acessados e menos com a interface do usuário, fica mais fácil para eles encontrar e resolver possíveis problemas de segurança.

Além disso, como há menos coisas para revisar na interface do usuário, as pessoas desenvolvedoras podem trabalhar e fazer mudanças nela mais rapidamente.

Isso facilita a aplicação de regras de segurança, tornando as atualizações e manutenções mais simples.

A centralização ajuda na monitoração eficiente do uso de dados, isolando componentes sensíveis e reduzindo riscos de ataques, como injeção de SQL.

Vale ressaltar que para a injeção de credenciais, é recomendado usar um arquivo .env, para evitar injetar credenciais no meio do seu código.

Deixo em especial este artigo sobre segurança em Next.js, que explora algumas abordagens sobre transferência de dados entre cliente e o servidor. O artigo está em inglês, mas você pode traduzir para Português, caso seja necessário.

Mas qual a diferença entre SSR e RSC?

Quando o Next.js carrega uma página pela primeira vez, ele roda tanto os Componentes do Servidor (RSC) quanto o Cliente no servidor para criar o HTML.

Os RSC funcionam separadamente dos Componentes do Cliente para evitar que informações se misturem acidentalmente.

"use server";
import axios from "axios";
import { revalidateTag } from "next/cache";
import { redirect } from "next/navigation";

export const addUser = async (data: FormData) => {
  // Logica para inserir os dados do form...
  const name = data.get("name")?.toString();
  const birthday = data.get("birthday")?.toString();
  const newUserBody = {
    name,
    birthday,
  };
  // Post user para o mock database
  await axios.post("http://localhost:3000/api/users", newUserBody);
  // Refetch User's
  revalidateTag("User");

  redirect("/");
}

O use server é uma declaração especial no Next.js que indica que o código a seguir é executado apenas no lado do servidor. Isso é parte da funcionalidade de componentes server-only introduzida em versões mais recentes do Next.js.

O exemplo acima, exemplifica como os Componentes do Servidor no Next.js podem ser usados para lidar com lógica de backend, como a manipulação de dados de formulários e interação com APIs, separando claramente essas operações da lógica do lado do cliente.

Ele garante que as operações sensíveis e específicas do servidor não sejam expostas ou executadas no lado do cliente, mantendo a segurança e a eficiência na renderização das páginas. Você notará que normalmente é dado o nome de Server Actions.

Para mais detalhes sobre Server e Client components na prática, recomendo a leitura do artigo Next: Server Actions aprendendo na prática.

Já os Componentes do Cliente, que são renderizados pelo servidor (SSR), têm as mesmas regras de segurança que um navegador, não podendo acessar dados restritos ou APIs privadas. É importante não tentar burlar essa segurança.

'use client'

import { minhaAction } from ‘@/app/actions’

export default function ClientComponent() {
  return <form action={ minhaAction }>{/* ... */}</form>
}

O Next.js impede a execução se os componentes do cliente tentarem usar recursos exclusivos do servidor, seguindo a ideia de que o código deve funcionar tanto no servidor quanto no cliente.

Maiores detalhes sobre Server e Client Components, recomendo a leitura do Componentes de servidor e cliente. O texto está em Inglês, mas você pode trazer para Português.

Como proteger roteiros de API e manipuladores de dados

Proteger as rotas da API e manipuladores de dados em Next.js envolve estratégias de autenticação confiáveis, podemos utilizar recursos como JWT ou NextAuth.js para sessões seguras e autenticação facilitada.

A escolha da estratégia depende da complexidade da aplicação e dos dados a serem protegidos. Vamos ver um exemplo:

import { getSession } from 'next-auth/react';

export default async function handler(req, res) {
    const session = await getSession({ req });
    if (!session) {
        res.status(401).json({ error: 'Não autorizado' });
        return;
    }
    // Lógica protegida aqui
    res.status(200).json({ success: true });
}

Além disso, é importante escolher o modelo certo para lidar com os dados, seja através de APIs HTTP, camada de acesso a dados ou acesso a dados no nível do componente, considerando a complexidade, quantidade de dados e autorização necessária.

Como implementar proteção contra CSRF e limitação de taxa

Outro ponto importante para garantir a segurança em aplicações Next.js é prevenir ataques de falsificação de solicitações entre sites (CSRF), que podem resultar em violações sérias de segurança, como roubo de contas, dados e instalação de malware.

Uma maneira eficaz de se proteger contra esses ataques é através do uso de tokens CSRF, os quais verificam cada solicitação feita do cliente para o servidor.

Você pode utilizar bibliotecas como o axios para facilitar a criação e o gerenciamento de tokens CSRF, que são automaticamente incluídos em cada cabeçalho de solicitação.

// pages/api/token-csrf.js
import { criarTokenCSRF } from '../../utils/csrf';

export default function handler(req, res) {
    const tokenCSRF = criarTokenCSRF();
    res.status(200).json({ tokenCSRF });
}

Em seguida, em um componente de formulário, importe e use esse token:

// O hook useCSRF
import { useState, useEffect } from 'react';
import { getTokenCSRF } from '../api/token-csrf';

function MeuFormulario() {
    const [tokenCSRF, setTokenCsrf] = useState('');

    useEffect(() => {
        async function buscarTokenCSRF() {
            const response = await getTokenCSRF();
            setTokenCsrf(response.tokenCSRF);
        }

        buscarTokenCSRF();
    }, []);

    // Use tokenCSRF em seu formulário
    // ...
}

A implementação de limitação de taxa nas suas rotas da API também é super importante.

Ela previne ataques de força bruta e negação de serviço (DoS) limitando o número de solicitações que um cliente pode fazer em um determinado período. Pacotes como express-rate-limit são úteis para isso.

// Limitação de taxa com express-rate-limit
import rateLimit from 'express-rate-limit';

const limitador = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutos
    max: 100, // limita cada IP a 100 solicitações por janela de tempo
    message: 'Muitas solicitações, tente novamente mais tarde.'
});

app.use(limitador);

Encontrar o equilíbrio entre usabilidade e segurança pode ser um desafio. Limites muito restritos podem afetar a experiência do usuário, enquanto restrições muito brandas podem não oferecer proteção adequada. Por isso, o equilíbrio é essencial.

Conclusão

Ao usar o Next.js como nosso aliado na jornada de construir uma aplicação, é obrigatório prestar atenção em cada peça, em cada parte, garantindo que tudo se encaixa de forma segura.

O cuidado com a segurança, especialmente relacionados com o gerenciamento de chaves secretas, a proteção contra ataques ao banco de dados, e a configuração adequada do CORS, é como garantir que todas as portas e janelas da casa estejam bem trancadas, não queremos tomar um susto durante a noite, com um desconhecido entrando pela janela.

Os React Server Components (RSC) no Next.js 14 surgem como aliados, oferecendo novas formas de montar este quebra-cabeça, com segurança reforçada no lado do servidor, reduzindo a superfície de ataque e protegendo dados sensíveis.

Nos ajudam mantendo informações confidenciais a salvo e minimizando as vulnerabilidades, especialmente contra ataques XSS.

É de extrema importância não apenas proteger os dados e rotas da API, mas também escolher a abordagem adequada para lidar com a transferência de dados entre o cliente e o servidor.

A implementação de tokens CSRF e a limitação de taxa são como blindagens adicionais, protegendo a aplicação contra invasões indesejadas e garantindo que somente solicitações legítimas sejam atendidas.

Em resumo, proteger uma aplicação Next.js é um processo contínuo e detalhado, implementação e validação de segurança é o tipo de trabalho que nunca acaba, pois frequentemente precisamos, revisar, validar e ajustar questões com segurança.

Basicamente, o que fazemos é uma combinação cuidadosa de ferramentas e estratégias.

Assim como um capitão que navega em mares desconhecidos, é essencial estar sempre atento e adaptar-se às novas ameaças, garantindo que o tesouro permaneça seguro.

Com essas práticas em mãos, você, pessoa desenvolvedora, pode navegar com confiança no vasto oceano da web, assegurando a integridade e a segurança de suas aplicações Next.js.

Espero que tenha chegado até aqui e que tenha gostado deste bate-papo ao redor da segurança.

Se quiser se aprofundar no assunto

Aqui na Alura, temos vários conteúdos sobre Next, vamos conhecê-las?

Aproveite e veja os conteúdos que a comunidade Next divulgou na última conferência.

Compartilhe o que você aprendeu

E vamos lançar um desafio! Se você gostou desse artigo, compartilhe nas redes sociais o que você aprendeu com ele com a hashtag #AprendinoBlogdaAlura.

Patrícia Silva
Patrícia Silva

Sou Engenheira de Software, atualmente atuando como Fullstack Engineer e baseada em Portugal. Sou uma profissional entusiasmada que ama tecnologia. Trabalho como desenvolvedora web há mais de 15 anos. Ajudo desenvolvedores a melhorar suas habilidades e estou aberta a trocas de conhecimento com qualquer pessoa. Sou mãe de plantas e de dois meninos lindos. Adoro viajar, conhecer novas pessoas e estar em contato com a natureza. O foco e o cuidado com a experiência do usuário são o segredo do sucesso.

Veja outros artigos sobre Front-end