Manual de Melhores Práticas: Firebase Security Rules
Guia completo para implementar segurança robusta no Realtime Database e Cloud Storage
Sérgio Ciarallo - Analista de STI - HSN Informática
Fundamentos
O Que São Firebase Security Rules?
Firebase Security Rules são regras declarativas que controlam o acesso aos seus dados no Realtime Database e arquivos no Cloud Storage. Elas funcionam como um firewall que valida cada requisição antes de permitir leitura ou escrita.
Ao contrário de validações no lado do cliente, as Rules são executadas no servidor do Firebase, tornando-as impossíveis de contornar. Isso garante que mesmo usuários maliciosos não consigam acessar dados não autorizados, independente de qual cliente estejam usando.
Cada regra pode avaliar a autenticação do usuário, validar a estrutura dos dados, verificar permissões personalizadas e até consultar outros nós do banco para decisões complexas de autorização.
Princípios Fundamentais de Segurança
Negar por Padrão
Sempre comece negando todo acesso e libere apenas o necessário. Nunca use regras abertas em produção.
Princípio do Menor Privilégio
Conceda apenas as permissões mínimas necessárias para cada usuário executar suas tarefas.
Validar Sempre
Valide tipo, formato e tamanho dos dados em cada operação de escrita para manter integridade.
Autenticar Primeiro
Use Firebase Auth para identificar usuários antes de conceder qualquer acesso aos dados.
Realtime Database
Estrutura Básica das Database Rules
As regras do Realtime Database são escritas em JSON e seguem uma estrutura hierárquica que espelha a estrutura do seu banco de dados. Cada nó pode ter regras de leitura (.read) e escrita (.write), além de validações (.validate).
{ "rules": { ".read": false, // Regra global de leitura ".write": false, // Regra global de escrita "users": { "$userId": { ".read": "auth != null && auth.uid == $userId", ".write": "auth != null && auth.uid == $userId", ".validate": "newData.hasChildren(['name', 'email'])" } } } }
As regras são avaliadas de cima para baixo, e se qualquer regra no caminho conceder acesso, a operação é permitida. Isso significa que você não pode revogar permissões em níveis mais profundos se já foram concedidas em níveis superiores.
Variáveis Disponíveis nas Rules
Variáveis de Autenticação
  • auth - Objeto com informações do usuário autenticado
  • auth.uid - ID único do usuário
  • auth.token - Claims customizados e provider info
  • auth.token.email - Email do usuário
  • auth.token.email_verified - Status de verificação
Variáveis de Dados
  • data - Dados atuais antes da operação
  • newData - Dados após a operação (em writes)
  • root - Referência à raiz do banco
  • $variables - Parâmetros de path dinâmicos
Exemplo Prático
{ "rules": { "posts": { "$postId": { ".read": true, ".write": "auth != null && (!data.exists() || data.child('authorId').val() == auth.uid)", ".validate": "newData.hasChildren(['title', 'body', 'authorId']) && newData.child('title').isString() && newData.child('body').isString() && newData.child('authorId').val() == auth.uid" } } } }
Este exemplo permite que qualquer um leia posts, mas apenas usuários autenticados criem novos posts, e apenas autores editem seus próprios posts.
Regra #1: Dados Privados do Usuário
O caso mais comum é permitir que usuários acessem apenas seus próprios dados. Esta regra garante que cada usuário tenha um espaço privado no banco.
{ "rules": { "users": { "$uid": { ".read": "auth != null && auth.uid == $uid", ".write": "auth != null && auth.uid == $uid" } } } }

Explicação: O $uid é uma variável que captura o ID do path. A regra verifica se o usuário está autenticado (auth != null) e se o ID dele corresponde ao path sendo acessado.
Regra #2: Dados Públicos de Leitura
Para conteúdo público que todos podem ler, mas apenas usuários autenticados podem criar ou editar.
{ "rules": { "publicPosts": { ".read": true, "$postId": { ".write": "auth != null && (newData.child('authorId').val() == auth.uid || data.child('authorId').val() == auth.uid)", ".validate": "newData.hasChildren(['title', 'content', 'authorId', 'timestamp'])" } } } }
Leitura Pública
Qualquer pessoa pode ler, mesmo sem autenticação
Escrita Controlada
Apenas o autor pode criar ou editar seu próprio post
Validação Obrigatória
Garante que todos campos necessários existem
Regra #3: Validação de Tipos de Dados
Validar o tipo e formato dos dados é crucial para manter a integridade do banco. Use os métodos isString(), isNumber(), isBoolean() e expressões regulares para validações complexas.
{ "rules": { "products": { "$productId": { ".write": "auth != null", ".validate": "newData.hasChildren(['name', 'price', 'category']) && newData.child('name').isString() && newData.child('name').val().length > 3 && newData.child('name').val().length <= 100 && newData.child('price').isNumber() && newData.child('price').val() > 0 && newData.child('category').isString() && newData.child('category').val().matches(/^(electronics|clothing|books)$/)", "name": { ".validate": "newData.isString()" }, "price": { ".validate": "newData.isNumber() && newData.val() >= 0" }, "category": { ".validate": "newData.isString()" }, "$other": { ".validate": false } } } } }

A última linha "$other": {".validate": false} impede que campos não especificados sejam adicionados, garantindo que apenas os campos definidos existam no documento.
Regra #4: Controle Baseado em Roles
Para aplicações com diferentes níveis de permissão, use custom claims no Firebase Auth combinados com regras que verificam esses roles.
{ "rules": { "adminPanel": { ".read": "auth != null && auth.token.admin == true", ".write": "auth != null && auth.token.admin == true" }, "moderatorContent": { ".read": "auth != null && (auth.token.moderator == true || auth.token.admin == true)", ".write": "auth != null && (auth.token.moderator == true || auth.token.admin == true)" }, "userProfiles": { "$uid": { ".read": "auth != null", ".write": "auth != null && (auth.uid == $uid || auth.token.admin == true)" } } } }
No seu backend (Node.js/Admin SDK), defina custom claims:
// Tornar usuário admin admin.auth().setCustomUserClaims(uid, { admin: true, moderator: true }); // Verificar claims const user = await admin.auth().getUser(uid); console.log(user.customClaims);
Custom claims são ideais para roles porque são validados no servidor e não podem ser adulterados pelo cliente.
Regra #5: Relações e Referências
Valide relacionamentos entre dados consultando outros nós do banco. Isso garante integridade referencial e previne dados órfãos ou inconsistentes.
{ "rules": { "comments": { "$commentId": { ".write": "auth != null && root.child('posts').child(newData.child('postId').val()).exists() && root.child('users').child(auth.uid).exists()", ".validate": "newData.hasChildren(['postId', 'userId', 'text', 'timestamp']) && newData.child('userId').val() == auth.uid && newData.child('text').isString() && newData.child('text').val().length > 0 && newData.child('text').val().length <= 500" } }, "userFollowers": { "$userId": { "$followerId": { ".write": "auth != null && auth.uid == $followerId", ".validate": "newData.val() == true && root.child('users').child($userId).exists()" } } } } }
A primeira regra garante que comentários só podem ser criados em posts existentes. A segunda permite seguir apenas usuários que existem no sistema.
Regra #6: Prevenção de Spam e Rate Limiting
Limite a frequência de criação de dados usando timestamps para prevenir spam e abuso.
{ "rules": { "userPosts": { "$userId": { "$postId": { ".write": "auth != null && auth.uid == $userId && (!root.child('userLastPost').child($userId).exists() || (now - root.child('userLastPost').child($userId).val()) > 60000)", ".validate": "newData.hasChildren(['content', 'timestamp']) && newData.child('timestamp').val() == now" } } }, "userLastPost": { "$userId": { ".write": "auth != null && auth.uid == $userId", ".validate": "newData.val() == now" } } } }
Esta técnica rastreia o último post do usuário e só permite novos posts após 60 segundos (60000 ms).
Intervalo Mínimo
60 segundos entre posts
Validação de Timestamp
Usa now do servidor
Previne Abuso
Bloqueia spam automático
Regra #7: Schemas Complexos com Validação Profunda
Para estruturas de dados complexas, valide cada nível da hierarquia separadamente para garantir conformidade total.
{ "rules": { "orders": { "$orderId": { ".write": "auth != null && newData.child('userId').val() == auth.uid", ".validate": "newData.hasChildren(['userId', 'items', 'total', 'status', 'createdAt'])", "userId": { ".validate": "newData.val() == auth.uid" }, "status": { ".validate": "newData.val() == 'pending'" }, "createdAt": { ".validate": "newData.val() == now" }, "total": { ".validate": "newData.isNumber() && newData.val() > 0" }, "items": { ".validate": "newData.hasChildren()", "$itemId": { ".validate": "newData.hasChildren(['productId', 'quantity', 'price'])", "productId": { ".validate": "newData.isString() && root.child('products').child(newData.val()).exists()" }, "quantity": { ".validate": "newData.isNumber() && newData.val() > 0 && newData.val() <= 100" }, "price": { ".validate": "newData.isNumber() && newData.val() > 0" }, "$other": { ".validate": false } } }, "$other": { ".validate": false } } } } }
Esta regra valida uma estrutura de pedido completa, incluindo verificação de produtos existentes e limites de quantidade.
Regra #8: Imutabilidade de Campos Críticos
Por Que Imutabilidade?
Certos campos não devem mudar após criação: IDs de autor, timestamps de criação, histórico de transações
Protege Integridade
Previne manipulação de dados críticos e mantém auditoria confiável
{ "rules": { "transactions": { "$txId": { ".write": "auth != null && (!data.exists() || auth.token.admin == true)", ".validate": "newData.hasChildren(['userId', 'amount', 'timestamp'])", "userId": { ".validate": "newData.val() == auth.uid && (!data.exists() || newData.val() == data.val())" }, "amount": { ".validate": "newData.isNumber() && (!data.exists() || newData.val() == data.val())" }, "timestamp": { ".validate": "!data.exists() && newData.val() == now" } } } } }
A condição !data.exists() || newData.val() == data.val() permite criar mas não modificar o campo.
Regra #9: Listas de Permissões (Whitelists)
Use listas de permissões armazenadas no banco para controle dinâmico de acesso sem precisar modificar as rules.
{ "rules": { "privateGroups": { "$groupId": { ".read": "auth != null && (root.child('groupMembers').child($groupId).child(auth.uid).exists() || root.child('groupAdmins').child($groupId).child(auth.uid).exists())", ".write": "auth != null && root.child('groupAdmins').child($groupId).child(auth.uid).exists()" } }, "groupMembers": { "$groupId": { "$userId": { ".write": "auth != null && root.child('groupAdmins').child($groupId).child(auth.uid).exists()", ".validate": "newData.val() == true" } } }, "groupAdmins": { "$groupId": { "$userId": { ".write": "auth != null && root.child('groupAdmins').child($groupId).child(auth.uid).exists()", ".validate": "newData.val() == true" } } } } }

Com esta estrutura, você gerencia membros e admins dinamicamente através do banco, e as regras automaticamente refletem essas mudanças sem necessidade de redeploy.
Cloud Storage
Introdução ao Firebase Storage Rules
Cloud Storage Rules protegem arquivos armazenados no Firebase Storage. Diferente do Realtime Database, as regras do Storage usam uma sintaxe baseada em CEL (Common Expression Language) que oferece mais flexibilidade e clareza.
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /{allPaths=**} { allow read, write: if false; } } }
Esta é a estrutura básica. O rules_version = '2' é obrigatório e habilita recursos modernos.
01
Declarar versão das rules
Sempre use version 2
02
Definir service
firebase.storage para Storage
03
Match no bucket
Especifica o bucket alvo
04
Match em paths
Define regras por caminho
Operações e Condições no Storage
Operações Disponíveis
  • read - Permite downloads (get, list)
  • write - Permite uploads (create, update, delete)
  • get - Download de arquivo específico
  • list - Listar arquivos em diretório
  • create - Criar novo arquivo
  • update - Modificar arquivo existente
  • delete - Remover arquivo
Use read e write para simplicidade, ou operações granulares para controle preciso.
Variáveis Úteis
  • request.auth - Info do usuário autenticado
  • request.auth.uid - ID do usuário
  • request.auth.token - Custom claims
  • request.resource - Arquivo sendo enviado
  • request.resource.size - Tamanho em bytes
  • request.resource.contentType - MIME type
  • resource - Arquivo existente (em updates)
  • resource.metadata - Metadados customizados
Storage Regra #1: Upload de Avatares de Usuário
Permite que usuários façam upload de suas próprias fotos de perfil com validações de tamanho e tipo.
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /users/{userId}/avatar.{extension} { allow read: if true; allow write: if request.auth != null && request.auth.uid == userId && request.resource.size < 5 * 1024 * 1024 && request.resource.contentType.matches('image/.*') && extension.matches('(jpg|jpeg|png|gif)'); } } }
Leitura Pública
Qualquer um pode ver avatares
Escrita Privada
Apenas dono pode modificar
Limite de 5MB
Previne uploads muito grandes
Apenas Imagens
Valida tipo e extensão
Storage Regra #2: Documentos Privados
Armazene documentos sensíveis que apenas o proprietário pode acessar, com validações de tipo de arquivo.
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /users/{userId}/documents/{document} { allow read: if request.auth != null && request.auth.uid == userId; allow create: if request.auth != null && request.auth.uid == userId && request.resource.size < 10 * 1024 * 1024 && request.resource.contentType in [ 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'image/jpeg', 'image/png' ]; allow delete: if request.auth != null && request.auth.uid == userId; } } }
Esta regra cria um espaço totalmente privado para documentos pessoais de cada usuário.
A lista de content types permite PDFs, documentos Word e imagens. O limite de 10MB previne uploads excessivos mantendo espaço para documentos razoavelmente grandes.
Note que separamos create de delete - isso permite adicionar regras diferentes para cada operação se necessário no futuro.
Storage Regra #3: Upload com Metadados Customizados
Use metadados para armazenar informações adicionais sobre arquivos e validar conteúdo baseado nesses metadados.
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /posts/{postId}/media/{mediaFile} { allow read: if true; allow create: if request.auth != null && request.resource.size < 20 * 1024 * 1024 && request.resource.contentType.matches('(image|video)/.*') && request.resource.metadata.uploadedBy == request.auth.uid && request.resource.metadata.postId == postId && request.resource.metadata.keys().hasAll(['uploadedBy', 'postId', 'description']); allow update: if request.auth != null && resource.metadata.uploadedBy == request.auth.uid && request.resource.metadata.uploadedBy == resource.metadata.uploadedBy && request.resource.metadata.postId == resource.metadata.postId; allow delete: if request.auth != null && resource.metadata.uploadedBy == request.auth.uid; } } }

No cliente, envie metadados assim: storageRef.put(file, {customMetadata: {uploadedBy: uid, postId: 'abc', description: 'Foto da festa'}})
Storage Regra #4: Controle por Roles e Admins
Combine custom claims do Auth com Storage rules para implementar controle baseado em funções.
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { // Área pública - só admins podem modificar match /public/{file} { allow read: if true; allow write: if request.auth != null && request.auth.token.admin == true; } // Área de moderadores match /moderation/{file} { allow read, write: if request.auth != null && (request.auth.token.moderator == true || request.auth.token.admin == true); } // Uploads de usuários verificados match /verified-uploads/{userId}/{file} { allow read: if true; allow write: if request.auth != null && request.auth.uid == userId && request.auth.token.email_verified == true && request.resource.size < 15 * 1024 * 1024; } } }
1
Área Pública
Leitura livre, escrita apenas admin
2
Moderação
Acesso total para moderadores e admins
3
Verificados
Apenas usuários com email verificado
Storage Regra #5: Validação de Imagens com Dimensões
Embora o Storage não possa validar dimensões diretamente nas rules, você pode usar Cloud Functions como camada adicional de validação.
Rules do Storage
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /gallery/{imageId} { allow read: if true; allow create: if request.auth != null && request.resource.size < 5 * 1024 * 1024 && request.resource.contentType.matches('image/(jpeg|png|webp)') && request.resource.metadata.validated != 'true'; allow update: if request.auth != null && resource.metadata.uploadedBy == request.auth.uid && request.resource.metadata.validated == 'true'; allow delete: if request.auth != null && (resource.metadata.uploadedBy == request.auth.uid || request.auth.token.admin == true); } } }
Cloud Function de Validação
const functions = require('firebase-functions'); const admin = require('firebase-admin'); const sharp = require('sharp'); exports.validateImage = functions.storage.object().onFinalize(async (object) => { const filePath = object.name; if (!filePath.startsWith('gallery/')) return; const bucket = admin.storage().bucket(); const file = bucket.file(filePath); const [buffer] = await file.download(); const metadata = await sharp(buffer).metadata(); if (metadata.width < 800 || metadata.height < 600) { await file.delete(); return; } await file.setMetadata({ metadata: { validated: 'true' } }); });
Storage Regra #6: Estruturas Hierárquicas
Organize arquivos em hierarquias complexas com permissões diferentes em cada nível.
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { // Organizações > Projetos > Arquivos match /organizations/{orgId}/projects/{projectId}/{allPaths=**} { // Ler se for membro da organização allow read: if request.auth != null && firestore.get(/databases/(default)/documents/organizations/$(orgId)/members/$(request.auth.uid)).data.role != null; // Escrever se for admin ou manager do projeto allow write: if request.auth != null && (firestore.get(/databases/(default)/documents/organizations/$(orgId)/members/$(request.auth.uid)).data.role in ['admin', 'owner'] || firestore.get(/databases/(default)/documents/organizations/$(orgId)/projects/$(projectId)/members/$(request.auth.uid)).data.role == 'manager'); } // Pasta compartilhada da organização match /organizations/{orgId}/shared/{file} { allow read: if request.auth != null && firestore.exists(/databases/(default)/documents/organizations/$(orgId)/members/$(request.auth.uid)); allow create: if request.auth != null && firestore.exists(/databases/(default)/documents/organizations/$(orgId)/members/$(request.auth.uid)) && request.resource.size < 50 * 1024 * 1024; } } }
Esta estrutura consulta o Firestore para verificar membros e roles, criando um sistema de permissões sofisticado.
Funções Auxiliares para Reutilização
Crie funções auxiliares para evitar repetição de lógica e tornar as rules mais legíveis e manuteníveis.
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { // Funções auxiliares function isAuthenticated() { return request.auth != null; } function isOwner(userId) { return isAuthenticated() && request.auth.uid == userId; } function isAdmin() { return isAuthenticated() && request.auth.token.admin == true; } function isValidImage() { return request.resource.contentType.matches('image/.*') && request.resource.size < 5 * 1024 * 1024; } function isValidDocument() { return request.resource.contentType in [ 'application/pdf', 'application/msword', 'text/plain' ] && request.resource.size < 10 * 1024 * 1024; }
// Usando as funções match /users/{userId}/avatar { allow read: if true; allow write: if isOwner(userId) && isValidImage(); } match /users/{userId}/documents/{doc} { allow read: if isOwner(userId) || isAdmin(); allow write: if isOwner(userId) && isValidDocument(); } match /admin/{file} { allow read, write: if isAdmin(); } } }
Funções tornam as regras muito mais legíveis e facilitam manutenção. Você define a lógica uma vez e reutiliza em múltiplos lugares.
Sempre declare funções no nível mais alto possível para maximizar reutilização.
Checklist de Segurança Firebase
1
Nunca Deploy com Rules Abertas
Jamais use .read: true e .write: true globalmente em produção. Sempre comece negando tudo.
2
Sempre Valide Dados na Escrita
Use .validate no Database e validações de contentType/size no Storage para garantir integridade.
3
Teste as Rules Localmente
Use o Firebase Emulator Suite para testar rules antes de fazer deploy. Execute testes automatizados.
4
Use Autenticação Sempre
Integre Firebase Auth e verifique auth != null em praticamente todas as regras de escrita.
5
Limite Tamanhos de Dados
Valide comprimento de strings, tamanho de arrays e size de arquivos para prevenir abuso.
6
Documente Suas Rules
Adicione comentários explicando a lógica, especialmente em regras complexas com múltiplas condições.
7
Monitore e Audite
Use Firebase Console para revisar tentativas de acesso negadas e identificar possíveis ataques.
8
Mantenha Rules Atualizadas
Revise e atualize rules regularmente conforme sua aplicação evolui e novos recursos são adicionados.
Conclusão: Segurança é Fundamental
Firebase Security Rules são sua primeira e mais importante linha de defesa. Quando implementadas corretamente, elas garantem que seus dados permaneçam seguros independente de vulnerabilidades no código do cliente.
Lembre-se: segurança não é um recurso opcional, é um requisito fundamental. Invista tempo para entender e implementar rules robustas desde o início do projeto.
Use os exemplos deste manual como ponto de partida, adapte-os para suas necessidades específicas, e sempre teste extensivamente antes de fazer deploy em produção.
Próximos Passos
  • Configure o Firebase Emulator Suite localmente
  • Escreva testes automatizados para suas rules
  • Implemente monitoramento de tentativas de acesso negadas
  • Revise e atualize rules a cada sprint
  • Documente decisões de segurança para a equipe
Com conhecimento e disciplina, você pode construir aplicações Firebase verdadeiramente seguras.