Skip to main content

5 práticas recomendadas de segurança para React com TypeScript

Escrito por

Marcelo Oliveira

feature typescript react

8 de dezembro de 2022

0 minutos de leitura

Como biblioteca voltada à criação de interfaces de usuário, e não um framework completo, o React permite que os desenvolvedores escolham as bibliotecas que preferirem para diferentes aspectos de uma aplicação, como roteamento, histórico e autenticação. Já o TypeScript foi criado pela Microsoft como uma extensão do JavaScript para adicionar tipagem estática opcional a uma linguagem que, de outra forma, tem tipagem fraca.

Usar TypeScript com React oferece várias vantagens no desenvolvimento de aplicações, como a possibilidade de criar componentes React mais simples e um suporte melhor a JavaScript XML (JSX) para validação de tipos estática. Como podemos usar componentes JavaScript em um projeto TypeScript, equipes de desenvolvimento com experiência em JavaScript podem aproveitar esse conhecimento para se beneficiar da programação com tipagem forte.

Há muitos boilerplates disponíveis para iniciar projetos React, incluindo Create React App, Create Next App, Vite, React Boilerplate e React Starter Kit.

O Create React App é uma ferramenta independente que pode ser executada com npm ou Yarn. Depois de instalada, podemos gerar e executar um novo projeto com apenas alguns comandos.

Primeiro, abra o terminal e execute o comando a seguir para instalar a ferramenta Create React App:

npm install -g create-react-app

Em seguida, crie um projeto usando o template TypeScript com o executor de pacotes do Node (npx):

npx create-react-app [webapp-name] --template typescript

Outra opção é criar o projeto usando o gerenciador de pacotes Yarn:

yarn create react-app [webapp-name] --template typescript

possíveis riscos de segurança e como mitigá-los.

Ative o modo estrito

O modo estrito ativa automaticamente os parâmetros do compilador TypeScript relacionados às regras de tipos de dados. Quando o modo estrito do TypeScript está ativado, o código é validado de acordo com regras rigorosas de tipagem, obrigando os desenvolvedores a respeitar as limitações dos tipos de dados atribuídos a variáveis, constantes, parâmetros e valores de retorno de funções. O modo estrito é importante porque ajuda os desenvolvedores a encontrar e corrigir erros de incompatibilidade de tipos logo no início do desenvolvimento.

Imagine que uma aplicação tenha a seguinte função:

export async function deleteComment(slug, commentId): Promise<void> {
  await axios.delete(`articles/${slug}/comments/${commentId}`);
}

Chamamos a função deleteComment passando o argumento slug como string e commentId como número:

deleteComment('whats-new-in-react', 1257);

No entanto, nada nos impede de chamar a função com duas strings:

deleteComment('foo', 'bar');

O código acima pode não ser permitido pelas regras de negócio, mas, como não especificamos os tipos, ambos os parâmetros são considerados do tipo padrão any. Por isso, o TypeScript não consegue nos ajudar a identificar o problema a menos que ativemos o modo estrito.

Novas aplicações React vêm com o valor de strict definido como true. No entanto, esse valor pode ser diferente em projetos existentes.

Para garantir que o projeto TypeScript esteja no modo estrito, abra o arquivo tsconfig.json e confira se o valor da configuração strict é true:

{
  ...
  "strict": true,
  ...
}

Volte à função deleteComment. Uma pequena alteração já gera erros de compilação do TypeScript:

Editor de TypeScript mostrando erros de tipos 'any' implícitos nos parâmetros slug e commentId em conduit.ts

Adicione os tipos string e number aos parâmetros de deleteComment:

export async function deleteComment(slug: string, commentId: number): Promise<void> {
  await axios.delete(`articles/${slug}/comments/${commentId}`);
}

Isso gera erros de compilação e impede os desenvolvedores de criar código cliente que chame a função deleteComment inadvertidamente com tipos inválidos, como no exemplo abaixo:

deleteComment(null, true);
Editor de TypeScript exibindo erros em conduit.ts: null passado como parâmetro string e boolean passado como parâmetro number.

A opção strict ativa automaticamente outras opções recomendadas do compilador, relacionadas à verificação de tipos mais rigorosa.

Não use o tipo de retorno any em callbacks cujo valor será ignorado

Se você declarar o tipo de retorno de um callback como any e, sem querer, usar o valor retornado quando a função não retorna nada, pode cometer um erro que passará despercebido.

Considere uma função chamada onListFieldKeyUp que recebe como parâmetro um callback chamado onEnter:

export function onListFieldKeyUp(onEnter: () => any): (ev: React.KeyboardEvent) => void {
  return (ev) => {
    if (ev.key === 'Enter') {
      ev.preventDefault();  
      var enterResult = onEnter();
      //do something with enterResult
      }
    }
  };
}

Observe que a variável enterResult acima armazena o resultado da função de callback onEnter para uso posterior. Embora o callback não retorne nenhum valor, declaramos a variável enterResult com o tipo any, então o TypeScript não consegue alertar você sobre o problema.

Qual é a solução?

Primeiro, se você sabe que a função onEnter não retorna nenhum valor, substitua o tipo any no parâmetro do callback por void. Agora, o TypeScript exibe o erro “Uma expressão do tipo 'void' não pode ser testada quanto à veracidade.”

Editor de código exibindo FormGroup.tsx com um erro de TypeScript: uma expressão do tipo 'void' não pode ser avaliada como verdadeira.

Por fim, remova o código que armazena o valor de retorno de onEnter na variável enterResult:

export function onListFieldKeyUp(onEnter: () => void): (ev: React.KeyboardEvent) => void {
  return (ev) => {
    if (ev.key === 'Enter') {
      ev.preventDefault();
      onEnter();
    }
  };
}

Ataques de renderização no lado do servidor em React

Uma aplicação web pode renderizar HTML no cliente ou no servidor. Frameworks e bibliotecas JavaScript modernos, como o React, adotam a renderização no lado do servidor. Essa abordagem traz ganhos de desempenho, incluindo o carregamento mais rápido das páginas: o back-end pode pré-renderizar rapidamente a página inteira e enviar o conteúdo estático de HTML, CSS e JavaScript para o front-end. Assim, os usuários podem navegar e visualizar a página web instantaneamente. Essa abordagem também ajuda na otimização para mecanismos de busca (SEO), pois páginas que carregam rapidamente têm pontuações mais altas nos algoritmos de busca.

Cross-site scripting (XSS) é um tipo de ataque em que invasores injetam scripts maliciosos do lado do cliente em uma página web. O React foi projetado para ser protegido contra XSS. No entanto, práticas de programação inadequadas e a renderização no lado do servidor em React podem gerar uma vulnerabilidade de XSS que será explorada por usuários mal-intencionados.

Por exemplo, nunca concatene dados não higienizados com a saída da função renderToStaticMarkup antes de enviar a string ao cliente:

app.get("/", function (req, res) {
  return res.send(
    ReactDOMServer.renderToStaticMarkup(
      React.createElement("h1", null, "Hello World!")
    ) + someUnsanitizedData
  );
});

O código acima não é seguro porque um invasor pode ter comprometido a variável someUnsanitizedData para incluir código JavaScript malicioso, como no exemplo a seguir:

someUnsanitizedData = "</scrïpt><scrïpt>alert('You are compromised!')</scrïpt>

Para evitar ataques de XSS, use um sanitizador de HTML, como o DomPurify.

Use tipos opacos

Um tipo de dado opaco impõe o ocultamento de informações. Sua estrutura de dados não é definida na interface, o que oculta e encapsula a implementação de um tipo de dado concreto. Módulos externos podem usar o tipo opaco sem acessar seus detalhes internos, enquanto funções internas com acesso às informações ocultas podem manipular o tipo. Os tipos opacos permitem alterar e evoluir detalhes internos sem mudar o código que os utiliza. Por isso, seu uso continua sendo uma prática recomendada de desenvolvimento.

Embora o TypeScript não ofereça tipos opacos nativamente, podemos implementá-los com facilidade. Vamos resolver um caso de uso real.

Imagine uma aplicação de comércio eletrônico que usa uma função em TypeScript para adicionar um produto ao carrinho do cliente:

function addToCart(customerCode: string, productCode: string) {
  console.log(`Product ${productCode} has been added to the cart of the customer ${customerCode}`);
}

Esses parâmetros tipados garantem que o programador não passe números ou outros tipos no lugar de strings para os parâmetros customerCode e productCode:

addToCart('ABC-001984', 'SPC-004487');

Embora não haja nada de errado com o código, o tipo não expressa os valores com precisão. Não dá para saber qual parâmetro é o código do cliente ou do produto apenas olhando a linha de código, e o TypeScript não emitirá um aviso se os valores forem trocados.

Agora veja como a função addToCart foi refatorada no exemplo abaixo para usar tipos opacos:

export type CustomerCode = string & { _: 'CustomerCode' };
export type ProductCode = string & { _: 'ProductCode' };

const makeCustomerCode =
  (customerCode: string): CustomerCode => {
    if (/^\w{3}-\d{6}$/.test(customerCode)) { //regex validation
      return customerCode as CustomerCode;
    } else {
      throw new Error('Not a customer code!');
    }
  };

const makeProductCode =
  (productCode: string): ProductCode => {
    if (/^\w{3}-\d{6}$/.test(productCode)) { //regex validation
      return productCode as ProductCode;
    } else {
      throw new Error('Not a product code!');
    }
  };

function addToCart(customerCode: CustomerCode, productCode: ProductCode) {
  console.log(`Product ${productCode} has been added to the cart of the customer ${customerCode}`);
}

let customerCode: CustomerCode = makeCustomerCode('ABC-001984');
let productCode: ProductCode = makeProductCode('SPC-004487');

addToCart(customerCode, productCode);

Graças aos novos tipos opacos e à validação com regex, o código gera um erro de compilação se você fornecer códigos incorretos:

let customerCode: CustomerCode = makeCustomerCode('000-001984'); //Error: Not a customer code!
let productCode: ProductCode = makeProductCode('9871'); //Error: Not a product code!

Quando usar dangerouslySetInnerHTML e adotar práticas adequadas de sanitização

O React usa um DOM virtual como estratégia leve para atualizar o HTML da página com eficiência, evitando que os usuários precisem lidar diretamente com APIs nativas do navegador para manipular elementos HTML.

No entanto, às vezes você pode precisar substituir esse mecanismo e definir código HTML bruto nas aplicações React. Para manipular esse código HTML diretamente, o React usa uma propriedade especial de componente chamada dangerouslySetInnerHTML:

return (
<p dangerouslySetInnerHTML={{__html: data}}></p>);

Como o nome sugere, a propriedade dangerouslySetInnerHTML deixa uma aplicação vulnerável quando não é usada corretamente. Invasores podem explorar aplicações que usam dangerouslySetInnerHTML para realizar ataques de cross-site scripting (XSS) e injetar scripts maliciosos disfarçados de entradas confiáveis de usuários no seu site.

Para evitar esse risco, sempre sanitize o conteúdo HTML antes de inseri-lo, removendo “impurezas” ou códigos maliciosos. Você pode usar uma biblioteca como DOMPurify e aplicar a função sanitize:

import DOMPurify from 'dompurify';

return (
<p dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(data)}}></p>);

É importante lembrar que, como qualquer outra biblioteca, o DOMPurify pode apresentar vulnerabilidades de segurança ocasionalmente. Felizmente, a Snyk Vulnerability Database incorporou rapidamente os dados dessa vulnerabilidade e forneceu uma solução. Portanto, mesmo que uma biblioteca tenha um histórico sólido, ainda precisamos fazer a nossa parte e usar uma ferramenta como a Snyk para verificar periodicamente se há vulnerabilidades conhecidas.

E essa é apenas uma das preocupações de segurança que os desenvolvedores devem considerar. Neste vídeo, Liran Tal explica como desenvolvedores React podem cometer erros que levam a outras vulnerabilidades exploráveis por invasores.

Liran Tal - You thought your React application is secure? Think again | ReactNext 2021

Conclusão

Neste artigo, apresentamos algumas práticas recomendadas para desenvolver aplicações React com TypeScript, destacamos possíveis riscos de segurança e indicamos soluções.

  • O modo estrito permite impor restrições de tipos e detectar erros de incompatibilidade de tipos logo no início do desenvolvimento. Usar void em callbacks impede que os desenvolvedores usem um valor de retorno quando o callback não retorna nada.

  • Ataques de injeção de comandos são uma ameaça séria à segurança. Evite criar código dinâmico com entradas suspeitas de usuários e use a função execFile em vez de exec.

  • Injeções de HTML também são uma ameaça séria à segurança. Ao usar dangerouslySetInnerHTML, sempre sanitize a marcação inserida para impedir que invasores adulterem entradas de usuários e injetem códigos maliciosos.

  • Por fim, os tipos opacos definem domínios significativos e ajudam você a validar tipos com mais facilidade e evitar duplicações.

É fácil usar TypeScript e React para criar aplicações rápidas e seguras, especialmente com frameworks como Create React App e aproveitando o conhecimento prévio de JavaScript.

Leia mais

Blog

Modelos de ponta encontraram as vulnerabilidades. Só o atacante encontrou as cadeias.

A análise estática encontrou as falhas, mas só os testes de ataque em aplicações ativas provaram como elas poderiam ser encadeadas para causar invasões. Uma comparação entre Evo COS, Claude Security e Claude Code Security.

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

Blog

Por que agentes de programação com IA continuam criando falhas de controle de acesso

Agentes de programação com IA podem gerar uma lógica de autorização que compila e passa pela revisão, mas permite que um tenant acesse os dados de outro. Saiba por que é difícil detectar falhas de controle de acesso e como evitá-las.