O Poder do Sistema de Tipos do TypeScript na Prevenção de Vulnerabilidades
O TypeScript não é apenas uma ferramenta para adicionar tipos estáticos ao JavaScript — é um escudo preventivo contra uma classe inteira de vulnerabilidades de segurança. Quando você trabalha com tipos explícitos, força comportamentos seguros e elimina superfícies de ataque que existem naturalmente em código dinamicamente tipado. A verdade é que muitas vulnerabilidades comuns nascem da falta de clareza sobre o tipo e a forma dos dados que circulam pelo seu código.
Neste artigo, exploraremos como aproveitar o sistema de tipos do TypeScript para construir aplicações mais seguras. Não vamos apenas escrever código que "funciona" — vamos escrever código que não pode falhar da forma que esperamos. A diferença é fundamental: no primeiro caso, confiamos em testes; no segundo, confiamos na linguagem.
Tipos Primitivos e Validação de Entrada
Compreendendo o Risco Real
A validação de entrada é a primeira linha de defesa contra injeção de código, XSS, SQL injection e outros ataques. Em JavaScript puro, você nunca sabe realmente o tipo de um valor até tentar usá-lo. Em TypeScript, você sabe, desde o momento em que o código é escrito.
Considere um exemplo clássico: um endpoint que recebe dados de um usuário. Sem tipos, qualquer coisa entra. Com tipos, apenas o que você definir entra — e o compilador garante isso.
// ❌ SEM TIPOS - Vulnerável
function processarUsuario(dados: any) {
// dados pode ser qualquer coisa. Malware? Sim.
const email = dados.email;
const idade = dados.idade;
// Você espera idade ser número, mas pode ser uma string maliciosa
if (idade > 18) {
console.log("Acesso permitido");
}
}
// ✅ COM TIPOS - Seguro
interface UsuarioInput {
email: string;
idade: number;
}
function processarUsuario(dados: UsuarioInput) {
// TypeScript garante que idade é número
// Se tentar passar string, falha na compilação
if (dados.idade > 18) {
console.log("Acesso permitido");
}
}
A diferença é sutil no código, mas profunda na segurança. O TypeScript verifica em tempo de compilação se você está passando os tipos corretos. Um atacante não pode forçar um string para dentro de um campo number — o servidor simplesmente rejeitará a requisição antes de qualquer lógica de negócio executar.
Literal Types para Enumerações Seguras
Quando você tem um conjunto finito e bem-definido de valores (como status de pedidos ou níveis de acesso), use literal types. Isso previne que valores inesperados circulem pelo seu código.
// ❌ INSEGURO - String genérica
function atualizarStatusPedido(pedidoId: string, status: string) {
// Um atacante poderia passar "cancelado_de_forma_fraudulenta"
// Você não controlaria quais estados são válidos
database.update({ id: pedidoId, status });
}
// ✅ SEGURO - Literal Types
type StatusPedido = "pendente" | "processando" | "entregue" | "cancelado";
function atualizarStatusPedido(pedidoId: string, status: StatusPedido) {
// Apenas estes 4 valores são aceitos. Ponto.
database.update({ id: pedidoId, status });
}
// Isto não compila:
atualizarStatusPedido("123", "cancelado_fraudulentamente"); // ❌ Erro do TypeScript
Literal types transformam valores em contratos. Não há negociação — ou você passa um valor válido, ou o código não executa.
Tipos Opcionais e Null Safety
O Problema do Null Coalescing
Um dos bugs mais comuns em produção é tentar acessar propriedades de null ou undefined. Em JavaScript, você só descobre em runtime. Em TypeScript com strictNullChecks, você descobre na compilação.
// ❌ COM strictNullChecks: false (padrão antigo)
interface Usuario {
nome: string;
email?: string;
}
function enviarEmail(usuario: Usuario) {
// Isto compila, mas pode quebrar em runtime
// Se email for undefined, você terá um erro
const comprimento = usuario.email.length;
}
// ✅ COM strictNullChecks: true (recomendado)
interface Usuario {
nome: string;
email?: string; // Explicitamente opcional
}
function enviarEmail(usuario: Usuario) {
// TypeScript exige verificação antes de usar
if (usuario.email) {
const comprimento = usuario.email.length;
// Agora é seguro acessar
}
}
// Ou use operador de coalescência nula
function enviarEmailAlternativo(usuario: Usuario) {
const email = usuario.email ?? "email-nao-fornecido@example.com";
console.log(email);
}
Ativar strictNullChecks no seu tsconfig.json é uma das mudanças mais impactantes que você pode fazer. Força seu time a lidar explicitamente com valores que podem ser ausentes, eliminando uma classe inteira de bugs.
Non-null Assertion e seus Perigos
Existe a tentation de usar ! para "desabilitar" a verificação null. Evite isso quando possível — é uma abertura intencional em seu escudo de segurança.
// ⚠️ Usar com extrema cautela
interface Dados {
valor?: string;
}
const dados: Dados = obterDadosDoBancoDados();
// Isto compila porque você "prometeu" ao TypeScript que valor existe
const resultado = dados.valor!.toUpperCase();
// Mas se valor for undefined, você terá um erro em runtime
// Use apenas quando tiver 100% de certeza
A única exceção legítima é quando você tem conhecimento de domínio que o TypeScript não consegue expressar — e mesmo assim, comente seu código explicando por que você sabe que é seguro.
Tipos Genéricos e Sanitização de Dados
Criando Wrappers de Tipo para Dados Perigosos
Em uma aplicação real, você trabalha com dados de múltiplas origens: APIs externas, formulários, cookies, headers HTTP. Nem todos são confiáveis. Você pode usar tipos genéricos para criar "wrappers" que explicitam quando um dado foi validado e quando não.
// Marca um dado como "ainda não validado"
export type Untrusted<T> = T & { readonly __untrusted: true };
// Marca um dado como "validado e seguro"
export type Trusted<T> = T & { readonly __trusted: true };
// Função que transforma Untrusted em Trusted após validação
function validarEmail(email: Untrusted<string>): Trusted<string> | null {
const regexEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (regexEmail.test(email as string)) {
return (email as string as any as Trusted<string>);
}
return null;
}
// Agora você pode forçar que certas funções só aceitam dados validados
function salvarNoDatabase(email: Trusted<string>) {
// Se você tentar passar um Untrusted, falha na compilação
database.insert({ email: email as string });
}
// Uso:
const emailDoBrowser: Untrusted<string> = document.getElementById("email")!.value as any;
const emailValidado = validarEmail(emailDoBrowser);
if (emailValidado) {
salvarNoDatabase(emailValidado); // ✅ Compila
}
// Isto não compila:
salvarNoDatabase(emailDoBrowser); // ❌ Erro: esperava Trusted, recebeu Untrusted
Este padrão força refatoração em todos os fluxos de dados. Qualquer valor que chegue da internet deve ser explicitamente validado antes de ser usado em operações sensíveis. Não é mágica — é discipline codificada no sistema de tipos.
Tipos Distributivos para Sanitização
Para casos onde você precisa sanitizar propriedades específicas de um objeto, tipos distributivos são poderosos:
// Remove campos sensíveis após sanitização
type SensitiveFields = "senha" | "token_api" | "cpf";
type Sanitized<T> = {
[K in keyof T]: K extends SensitiveFields ? never : T[K];
};
interface Usuario {
id: number;
nome: string;
email: string;
senha: string;
token_api: string;
}
function retornarParaFrontend(usuario: Usuario): Sanitized<Usuario> {
// TypeScript força você a remover os campos sensíveis
const { senha, token_api, ...seguro } = usuario;
return seguro as Sanitized<Usuario>;
}
// Se você tentar incluir um campo sensível, falha:
// return { ...usuario } as Sanitized<Usuario>; // ❌ Erro
Prevenção de Ataques Comuns Através de Tipos
CSRF e Validação de Estado
Ataques CSRF funcionam porque um navegador enviará automaticamente cookies com qualquer requisição. Você pode usar o sistema de tipos para garantir que certas operações sensíveis sempre passam por validações específicas.
// Token que prova que a operação foi iniciada no seu site
type CSRFToken = string & { readonly __brand: "CSRFToken" };
function gerarCSRFToken(): CSRFToken {
return (Math.random().toString(36) as any as CSRFToken);
}
function validarCSRFToken(token: unknown, expected: CSRFToken): token is CSRFToken {
return token === expected;
}
// Operações sensíveis EXIGEM um token válido
function transferirDinheiro(
de: string,
para: string,
valor: number,
token: CSRFToken // ✅ Obrigatório
) {
// Processar transferência
console.log(`Transferindo ${valor} de ${de} para ${para}`);
}
// Isto não compila:
transferirDinheiro("conta1", "conta2", 1000); // ❌ Erro: falta token
// Para usar, precisa obter um token válido primeiro
const meuToken = gerarCSRFToken();
transferirDinheiro("conta1", "conta2", 1000, meuToken); // ✅ Ok
XSS Prevention com Tipos de String Especializados
Para aplicações que renderizam HTML no servidor ou no cliente, você pode criar tipos que marcam strings como "seguras" (já escapadas).
// String que foi escapada para HTML
type SafeHTML = string & { readonly __html: true };
// Função de escape que retorna o tipo correto
function escapeHTML(texto: string): SafeHTML {
const mapa: { [key: string]: string } = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
};
return texto.replace(/[&<>"']/g, (char) => mapa[char]) as SafeHTML;
}
// Funções de renderização só aceitam HTML seguro
function renderHTML(html: SafeHTML): string {
return html; // Seguro porque foi escapado
}
// Isto não compila:
const usuarioInput = "<img src=x onerror='alert(1)'>";
renderHTML(usuarioInput); // ❌ Erro: esperava SafeHTML, recebeu string
// Isto compila:
renderHTML(escapeHTML(usuarioInput)); // ✅ Ok - foi escapado
O compilador não deixará você renderizar HTML não-escapado. Ponto.
Conclusão
Aprendemos três lições fundamentais neste artigo. Primeiro: tipos explícitos são prevenção de bugs. Quando você define tipos rigorosos, elimina superfícies de ataque antes do código ser executado. O TypeScript verifica em compilação o que levaria horas de testes e debugging em JavaScript puro.
Segundo: null safety e validação de entrada são não-negociáveis. strictNullChecks força você a lidar com valores ausentes, e tipos genéricos como Untrusted<T> e Trusted<T> criam contratos que obrigam validação em pontos críticos. Não é burocracia — é segurança.
Terceiro: tipos especializados (branded types) codificam conhecimento de segurança. Em vez de confiar em comentários ou documentação, você força comportamentos seguros através do compilador. Um CSRFToken é diferente de uma string comum; HTML escapado é diferente de HTML bruto. Seu código se torna uma máquina de estados tipada que naturalmente segue as melhores práticas de segurança.