Desenvolver aplicativos híbridos com React Native é rápido e produtivo: uma única base de código em JavaScript/TypeScript roda em Android e iOS. Mas essa mesma conveniência traz um detalhe importante de segurança que muita gente esquece: o seu código roda no dispositivo do usuário. O bundle JavaScript pode ser extraído do APK/IPA, o armazenamento local pode ser inspecionado em aparelhos com root ou jailbreak, e qualquer segredo embutido no app pode ser lido por alguém determinado.
A ideia deste artigo é mostrar, de forma didática e com exemplos reais, as boas práticas mais importantes para desenvolver apps React Native mais seguros. Nada de teoria complicada: cada tópico traz o problema comum, a solução recomendada e um exemplo de código simples de entender.
Antes de tudo, guarde esta ideia central: segurança mobile funciona em camadas. Não existe uma única biblioteca que resolve tudo. O que existe é a soma de várias pequenas boas práticas no cliente somadas a um backend que nunca confia cegamente no app.
Regra de ouro: nunca confie apenas no cliente
Essa é a regra mais importante de todas. Tudo que roda no app pode ser inspecionado, modificado ou burlado. Validações e checagens no aplicativo servem para melhorar a experiência do usuário (UX), não para garantir segurança de verdade.
Pense em uma analogia simples: o app é a porta da frente da sua casa — ela precisa estar trancada, mas o cofre com o que realmente importa fica no servidor.
Um erro clássico é esconder um botão de "Admin" no JavaScript e assumir que a funcionalidade está protegida:
// Ruim: esconder o botão no app e achar que está seguro
{user.isAdmin && <AdminButton />}
// Se a API não validar, qualquer um pode chamar o endpoint direto.
// Bom: o app pode esconder o botão por UX,
// mas a API SEMPRE valida a permissão e retorna 403 se não for admin.A regra prática: toda decisão de autorização (quem pode ver, criar, editar ou apagar algo) precisa ser tomada e validada no backend. O app apenas reflete o que o servidor permite.
Armazenamento seguro de tokens e dados sensíveis
Problema: usar AsyncStorage para guardar informações sensíveis como JWT, refresh token, CPF ou dados de cartão. O AsyncStorage é um armazenamento simples, sem criptografia, e pode ser lido facilmente em dispositivos comprometidos.
Solução: usar um armazenamento seguro fornecido pelo sistema operacional. No Expo, use o expo-secure-store; em projetos "bare", use o react-native-keychain. Eles guardam os dados no Keychain (iOS) e no Keystore/EncryptedSharedPreferences (Android).
A dúvida comum é: o que é sensível e o que não é? A tabela abaixo ajuda a decidir:
| Cenário | Ruim | Bom |
|---|---|---|
| Token de login (JWT) | AsyncStorage.setItem('token', jwt) | SecureStore.setItemAsync('token', jwt) |
| Refresh token | AsyncStorage | SecureStore / Keychain |
| CPF, dados bancários | AsyncStorage | SecureStore / Keychain |
| Preferência de tema (claro/escuro) | AsyncStorage (ok — não é sensível) | AsyncStorage |
| Flag "já viu o onboarding" | AsyncStorage (ok — não é sensível) | AsyncStorage |
Um módulo simples para guardar e ler o token de forma segura fica assim:
import * as SecureStore from 'expo-secure-store';
const TOKEN_KEY = 'auth_token';
export async function saveToken(token: string): Promise<void> {
await SecureStore.setItemAsync(TOKEN_KEY, token);
}
export async function getToken(): Promise<string | null> {
return SecureStore.getItemAsync(TOKEN_KEY);
}
export async function clearToken(): Promise<void> {
await SecureStore.deleteItemAsync(TOKEN_KEY);
}Um ponto honesto sobre os limites: o SecureStore protege muito bem contra acesso casual e contra outros apps, mas não é uma proteção absoluta em dispositivos com root ou jailbreak. Por isso a regra de ouro continua valendo — o backend deve sempre poder invalidar tokens.
Segredos e variáveis de ambiente
Problema: deixar chaves de API secretas "hardcoded" no código ou expostas ao app. Muita gente acredita que colocar uma chave em uma variável de ambiente já a torna secreta — mas no React Native, tudo que vai para o app vai junto no bundle.
Atenção especial ao prefixo EXPO_PUBLIC_: como o próprio nome diz, essas variáveis são públicas e ficam embutidas no bundle. Elas são ótimas para valores não sensíveis (como a URL da API), mas nunca para segredos.
Solução:
- Chaves realmente privadas (Stripe secret key, chave de serviços de terceiros, credenciais) ficam no backend. O app conversa com o seu servidor, e o servidor conversa com o terceiro (padrão BFF / proxy).
- No app, exponha apenas o que pode ser público, como a URL base da API.
- Em builds com EAS, use EAS Secrets e nunca faça commit do arquivo
.env.
// Ruim: chave secreta embutida no app (vai parar no bundle!)
const STRIPE_SECRET = 'sk_live_abc123';
await fetch('https://api.stripe.com/v1/charges', {
headers: { Authorization: `Bearer ${STRIPE_SECRET}` },
});
// Bom: o app chama o SEU backend, que guarda a chave secreta com segurança.
const API_URL = process.env.EXPO_PUBLIC_API_URL; // valor público, tudo bem
await fetch(`${API_URL}/payments/charge`, { method: 'POST' });Comunicação segura com a API
A troca de dados entre o app e o servidor é um dos pontos mais visados. Três cuidados resolvem a maioria dos problemas:
- HTTPS obrigatório. Nunca use HTTP em produção. No Android, garanta
android:usesCleartextTraffic="false"; no iOS, mantenha o App Transport Security (ATS) ativo. Assim o tráfego vai criptografado e evita ataques do tipo "man-in-the-middle". - Envie o token de forma consistente. Anexe o JWT vindo do SecureStore em cada requisição autenticada — e nunca registre esse header em logs.
- Trate o 401 (não autorizado). Quando o token expira, limpe a sessão e leve o usuário de volta ao login.
Dá para centralizar tudo isso em uma função fetch simples, sem depender de bibliotecas pesadas:
import { getToken, clearToken } from './secure-storage';
const API_URL = process.env.EXPO_PUBLIC_API_URL;
export async function apiFetch(path: string, options: RequestInit = {}) {
const token = await getToken();
const response = await fetch(`${API_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers,
},
});
if (response.status === 401) {
await clearToken();
throw new Error('Sessão expirada. Faça login novamente.');
}
return response;
}Para aplicativos com requisitos mais fortes (bancos, saúde, fintechs), vale considerar o certificate pinning — uma camada extra que garante que o app só confie no certificado do seu servidor, dificultando ainda mais ataques de interceptação. Bibliotecas como a react-native-ssl-pinning ajudam nisso. Não é obrigatório para todo app, então adote quando o risco justificar.
Validação de entrada no app
Validar os dados que o usuário digita antes de enviar para a API melhora a experiência e evita requisições desnecessárias. Uma opção limpa e bastante usada em projetos React Native é o zod, que descreve o formato esperado e valida com segurança de tipos.
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email('E-mail inválido'),
password: z.string().min(8, 'A senha precisa ter ao menos 8 caracteres'),
});
// Antes de enviar ao backend:
const result = loginSchema.safeParse(formData);
if (!result.success) {
// exibir os erros na UI, sem nem chamar a API
return;
}
await apiFetch('/login', { method: 'POST', body: JSON.stringify(result.data) });O detalhe importante: essa validação é para UX. Ela não substitui a validação no servidor. Sempre revalide os mesmos dados no backend, porque uma requisição pode chegar por fora do app.
Deep links e navegação
Problema: aceitar qualquer URL recebida por deep link (via Linking ou expo-router) e navegar diretamente para a rota indicada, sem validar. Um link malicioso pode tentar levar o usuário para telas indevidas ou passar parâmetros perigosos.
Solução: manter uma lista das rotas permitidas (whitelist) e validar os parâmetros antes de navegar.
// Ruim: confiar na rota que veio de fora
navigation.navigate(routeFromUrl);
// Bom: só navega se a rota estiver na lista permitida
const allowedRoutes = ['Home', 'ProductDetails'] as const;
type AllowedRoute = (typeof allowedRoutes)[number];
function safeNavigate(route: string) {
if (!allowedRoutes.includes(route as AllowedRoute)) return;
navigation.navigate(route as AllowedRoute);
}Logs e observabilidade
Problema: deixar console.log com dados sensíveis, como token, senha, CPF ou o payload completo de uma resposta. Esses logs podem vazar em ferramentas de monitoramento, em builds de produção ou no console de quem estiver depurando o aparelho.
Solução:
- Restrinja logs de depuração ao ambiente de desenvolvimento usando a flag global
__DEV__. - Nunca registre credenciais ou dados pessoais.
- Se usar ferramentas como Sentry ou Crashlytics, configure o "scrubbing" (remoção) de dados pessoais (PII).
if (__DEV__) {
console.log('Tentativa de login para', email); // ok apenas em dev
}
// Nunca faça isso:
// console.log('Token:', token);
// console.log('Resposta completa:', response);Proteções adicionais (visão geral)
Dependendo do tipo de app, vale conhecer algumas camadas extras. Não são obrigatórias para todo projeto, mas é bom saber que existem:
- Detecção de root/jailbreak: com
expo-deviceou bibliotecas comojail-monkey, dá para detectar aparelhos comprometidos e degradar ou bloquear funcionalidades sensíveis. - Ofuscação de código: o ProGuard/R8 no Android dificulta a leitura do código, mas não substitui um backend seguro — apenas eleva o esforço necessário para engenharia reversa.
- Biometria: o
expo-local-authenticationé ótimo para reabrir uma sessão de forma conveniente (Face ID / digital), mas nunca deve substituir a autenticação real feita no servidor. - Atualizações OTA assinadas: ao usar EAS Update para entregar atualizações de JS pela internet, mantenha a assinatura de bundles ativada para evitar injeção de código malicioso.
Para se aprofundar, a referência mais conhecida da área é o OWASP Mobile Top 10, que lista os riscos mais comuns em aplicações mobile.
Checklist final
Antes de publicar seu app, use esta lista rápida como revisão:
- [ ] Tokens e dados sensíveis guardados em SecureStore/Keychain (nunca em
AsyncStorage) - [ ] Nenhum segredo embutido no bundle (chaves privadas ficam no backend)
- [ ] HTTPS obrigatório em produção
- [ ] Validação de entrada no app e revalidação no backend
- [ ] Autorização sempre validada no servidor (regra de ouro)
- [ ] Respostas 401 tratadas (limpar sessão e redirecionar ao login)
- [ ] Deep links com rotas validadas por whitelist
- [ ] Logs sem token, senha ou dados pessoais
- [ ] Permissões mínimas (câmera, localização etc. só quando realmente necessário)
Conclusão
React Native acelera muito o desenvolvimento mobile, mas não elimina a sua responsabilidade com segurança. A maior parte dos vazamentos e problemas vem de descuidos simples: um token no AsyncStorage, uma chave secreta no bundle, um console.log esquecido ou uma API que confia cegamente no app.
A boa notícia é que as boas práticas aqui são baratas de aplicar e trazem um enorme ganho. Adote-as desde o começo do projeto, sempre lembrando da regra de ouro: o app é a porta, o cofre fica no servidor.
Se você quer continuar evoluindo na stack nativa do React Native, um bom próximo passo é entender como o JavaScript conversa com o código nativo — assunto que abordo no artigo sobre a nova arquitetura de Turbo Native Modules.