Seu frontend fala direto com o banco de dados. Veja como proteger o backend

Seu JavaScript expõe App ID e chave de cliente por design; o que eles podem fazer depende do backend. Medimos uma classe aberta (tudo), fechamos com quatro travas e medimos de novo, incluindo os 22 minutos em que a trava ficou meio aberta.

Doze requisições de quatro identidades contra uma classe, antes e depois de travá-la em 17 de setembro de 2026: com a classe aberta toda requisição passa; com permissões por classe e uma ACL do dono, anônimos e outros usuários recebem 404 código 101 e só o dono e a master key passam

Tradução do original em inglês, testado em 17 de setembro de 2026. Código, números e datas são os do original. Read in English

Abra seu app no navegador, abra as ferramentas de desenvolvedor e olhe a aba de rede. Dois headers saem com cada requisição ao seu backend: um App ID e uma chave. Qualquer pessoa lendo sua página tem os dois. Isso não é um vazamento. É assim que um frontend que fala com um backend gerenciado deve funcionar.

O que esses dois headers podem fazer é a questão inteira. Numa classe com permissões padrão, a resposta é: tudo. Listar todas as linhas, alterar qualquer linha, apagar todas. Medimos isso, doze requisições de quatro identidades, e depois fechamos a classe com quatro travas e medimos de novo após cada uma.

As quatro travas são permissões por classe, uma ACL por linha carimbada por um hook no backend, uma role verificada dentro de uma Cloud Function e uma master key (chave mestra) que nunca sai do servidor. Tudo abaixo foi medido em 17 de setembro de 2026, num backend Back4app, incluindo os 22 minutos em que a trava 1 ficou só parcialmente fechada.

Pegue o código funcionando: o repositório companion está em github.com/templates-back4app/lockdown-lab — probe.sh, seed.sh, lockdown.sh, cloud/main.js e migrate-acl.sh. Rode a sonda antes e depois de cada trava e você obtém as tabelas deste post para o seu próprio backend.

O que qualquer pessoa com a sua JavaScript key pode fazer agora?

Criamos uma classe Note do jeito que a maioria dos apps faz: a primeira escrita do cliente a criou. Depois semeamos dois usuários, ana e bob, e uma role moderator contendo bob, e rodamos doze requisições de quatro identidades. Anônimo significa só o App ID e a JavaScript key. A master key é o que um servidor teria.

Arquitetura: quatro identidades chegam à classe Note através de quatro travas em ordem: permissões por classe, um hook beforeSave que carimba uma ACL só do dono, uma verificação de role dentro de uma Cloud Function e uma master key que nunca sai do backend

Este é o estado 0, a classe como nasceu, às 15:08 UTC:

Identidade → requisiçãoHTTP / código
anônimo (JS key) → listar notas200
anônimo (JS key) → criar nota201
anônimo (JS key) → apagar a nota da ana200
ana (session token) → listar, ler, atualizar, criar200 / 200 / 200 / 201
bob (session token) → ler a nota da ana200
bob (session token) → atualizar a nota da ana200
bob (session token) → apagar a nota da ana200
master key → ler, apagar a nota da ana200 / 200

Toda linha verde. A terceira é a que merece atenção: alguém que nunca fez login, segurando apenas o que a sua página entrega, apagou os dados de outro usuário e recebeu um 200 por isso. Nada estava mal configurado. Esse é o padrão.

Medido · 17 de setembro de 2026 · classe Note, permissões padrão
12 / 12requisições permitidas, incl. delete anônimo
→
6 / 12permitidas após quatro travas: dono + master key
22 minaté a trava 1 valer em todos os servidores

Como medimos: probe.sh, doze requisições curl de quatro identidades contra linhas novas, as destrutivas por último, rodado pelo menos uma vez por estado (três vezes para a classe aberta) em 17 de setembro de 2026.

A tabela de propagação vem de um amostrador de dez requisições anônimas de listagem a cada 30 segundos; o servidor que respondeu é o header x-backend-server.

Por que a chave no seu frontend não é o problema?

Porque ela nunca foi um segredo. A JavaScript key, a Client key e a REST key são chaves de cliente: dizem qual app está chamando, para que o backend encontre seus dados e aplique suas regras. Elas foram feitas para estar num APK, numa página, num comando curl. Rotacioná-las não compra nada se as regras estão abertas.

A master key é o oposto. Ela ignora toda permissão deste post. Seu lugar é no Cloud Code, nas variáveis de ambiente de um servidor ou num script na sua máquina, e em nenhum cliente de nenhum tipo. O .env do repositório a guarda para as sondas e está no git-ignore por esse motivo.

A chave diz qual app está chamando. As permissões dizem o que ele pode fazer. Só uma dessas coisas é trabalho seu.

Então o trabalho não é esconder a chave. O trabalho é dizer ao backend, por classe e por linha, o que cada identidade pode fazer. Isso acontece em três lugares: nas permissões da classe, na ACL de cada linha e no código que roda no backend.

Como as permissões por classe barram requisições anônimas?

As permissões por classe (CLP) são a porta da classe. Para cada operação (find, get, create, update, delete, add field) elas dizem quem pode bater: todo mundo, só usuários autenticados, usuários e roles específicos, ou ninguém. A trava 1 é Authenticated (só usuários autenticados) em toda operação, o que deixa as linhas anônimas vermelhas e não mexe em mais nada.

A dashboard configura isso em Database → menu da classe → Security → Class Level Permission: desmarque Public, marque Authenticated em Read e Write. Fizemos a mesma coisa pelo terminal, pela schema API, porque um script é repetível e uma captura de tela não é:

# Stack: bash + curl (schema API, master key) | File: lockdown.sh
CLP='{"find":{"requiresAuthentication":true},"get":{"requiresAuthentication":true},"create":{"requiresAuthentication":true},"update":{"requiresAuthentication":true},"delete":{"requiresAuthentication":true},"addField":{}}'
curl -s -H "X-Parse-Application-Id: $APP_ID" -H "X-Parse-Master-Key: $MASTER_KEY" -H "Content-Type: application/json" \
  -X PUT "https://parseapi.back4app.com/schemas/Note" -d "{\"classLevelPermissions\":$CLP}"

O diálogo Edit Class Level Permissions da classe Note na dashboard: Public desmarcado em Read, Write e Add field, Authenticated marcado em Read e Write, e um campo Add Role

Com a CLP no lugar, um create anônimo foi recusado na hora: 404, código 101, Permission denied, user needs to be authenticated. Um count anônimo recebeu um par diferente, 400 / 119. Dois códigos para “não permitido”, dependendo da operação; trate os dois.

Quanto tempo uma mudança de permissão leva para chegar a todos os servidores?

Mais do que uma requisição, e essa foi a surpresa do dia. Mudamos a CLP às 15:08:40 UTC e sondamos às 15:10: o create anônimo foi recusado, a listagem anônima ainda era respondida. Vinte requisições de listagem seguidas deram 14 × 200 e 6 × 404, da mesma chave, no mesmo segundo. GET /schemas/Note mostrou as novas permissões o tempo todo. O caminho da requisição, não.

Deixamos um amostrador rodando: dez requisições anônimas de listagem a cada 30 segundos. Entre 15:14 e 15:30 o número de permitidas oscilou entre 10 de 10 e 0 de 10 e voltou.

O header de resposta x-backend-server explicou: todo 200 veio de um servidor, shared752, enquanto as negativas não traziam esse header. O app roda em várias instâncias, e uma delas manteve a regra antiga.

Restart App na página Overview às 15:25 informou restarted successfully e não mudou nada: 0 de 10 às 15:28, depois 8 de 10 de novo. O que encerrou a história foram dois deploys de Cloud Code por volta das 15:30, de um arquivo que não tinha mudado. A amostra seguinte leu 2 de 10, e toda amostra depois de 15:31:26 leu 0 de 10, pelo resto da tarde.

Hora (UTC)Requisições anônimas de listagem permitidas, de 10
15:14 – 15:1610, 10, 10, 10
15:16 – 15:306, 9, 8, 8, 7, 6, 10, 10, 10, 10, 7, 6, 8, 7, 10, 8, 9, 6, 5, 4, 0, 8, 8, 7, 9
15:30:53 (depois dos deploys)2
A partir de 15:310, 0, 0, … (24 amostras, até pararmos às 15:44)

Como um hook no backend dá a cada linha uma ACL só do dono?

A trava 1 mantém estranhos do lado de fora. Ela não faz nada quanto ao bob, que está logado e ainda pode ler, editar e apagar a nota da ana. Essa é a trava 2, a ACL: uma lista por linha de quem pode ler e quem pode escrever. O erro clássico é deixar o cliente defini-la; o cliente pode esquecer, e um comando curl vai esquecer. Então o backend a define, antes de a linha existir:

// Stack: Node.js 22.x | Parse Server 8.x | File: cloud/main.js
// Lock 2 of 4: every Note gets an ACL that only its owner can read or write. The client cannot forget to set it,
// because the client never sets it — the backend does, before the row exists.
Parse.Cloud.beforeSave("Note", (request) => {
  const note = request.object;
  if (!note.isNew()) return;                                   // ACL is decided once, at creation
  const owner = request.user ?? note.get("owner");             // request.user = whoever holds the session token
  if (!owner) throw new Parse.Error(Parse.Error.SESSION_MISSING, "log in to create a note.");
  const acl = new Parse.ACL(owner);                            // owner: read + write; everyone else: nothing
  note.setACL(acl);
  note.set("owner", owner);
});

request.user é quem enviou o session token, resolvido pelo backend; o cliente não consegue alegar ser outra pessoa. new Parse.ACL(owner) concede leitura e escrita a esse usuário e nada a mais ninguém, incluindo o público.

O pointer owner é definido no mesmo gesto, para que uma query possa filtrar por ele e a função de moderador possa mostrar quem escreveu o quê.

O editor de Cloud Code com cloud/main.js aberto no backend lockdown-lab: o hook beforeSave de Note que carimba a ACL do dono, e a Cloud Function moderatorList logo abaixo

Depois do deploy, toda nota que a ana cria carrega ACL: { "<id da ana>": { read: true, write: true } }, que o Database Browser mostra como o object id dela na coluna ACL. As linhas não vieram de um formulário que escrevemos; foram escritas pelas sondas, e o hook fez o resto.

O Database Browser mostrando a classe Note com catorze objetos: a coluna ACL de toda linha traz o object id da ana, EBkE7EJ1sb, e a coluna owner aponta para o mesmo usuário

O que o bob vê quando pede a nota da ana?

Nada, e não uma recusa. Com a ACL no lugar, bob lendo a nota da ana pelo id recebeu 404 / 101 Object not found. O mesmo status e código que um id inventado recebe. Atualizar e apagar: a mesma coisa. A ACL não diz você não pode; ela torna a linha invisível, e uma requisição de listagem simplesmente a omite.

Esse é o comportamento que você quer de um recurso de segurança e o comportamento que confunde um desenvolvedor às 2 da manhã. Se seus logs mostram 101 numa linha que você enxerga na dashboard, verifique a ACL antes de verificar o id.

Identidade → requisiçãoClasse abertaCLP + ACL + role
anônimo → listar, criar, apagar200 / 201 / 200404 / 101 nas três
ana → ler, atualizar a própria nota200 / 200200 / 200
ana → listar, criar200 / 201200 / 201
bob → ler, atualizar, apagar a nota da ana200 / 200 / 200404 / 101 nas três
master key → ler, apagar a nota da ana200 / 200200 / 200

Uma nota sobre a única coisa que vimos e não conseguimos reproduzir. Na primeira rodada da sonda, segundos depois de seed.sh criar os usuários, as escritas da ana devolveram 209 Invalid session token enquanto as leituras devolviam 200. Um minuto depois, três rodadas consecutivas não tiveram nenhum 209. Trate um 209 logo após o cadastro como motivo para rodar de novo, não como regra.

Como uma role deixa moderadores lerem tudo sem abrir a classe?

Trava 3. Um moderator precisa ver todas as notas, e o jeito errado de dar isso é afrouxar a CLP ou adicionar a role à ACL de cada linha. O jeito certo é uma Cloud Function que verifica a role, faz a leitura no backend com a master key e devolve exatamente o que um moderador deve ver:

// Stack: Node.js 22.x | Parse Server 8.x | File: cloud/main.js (continued)
// Lock 3 of 4: a role. Moderators can read everything through this function; nobody can through the class.
Parse.Cloud.define("moderatorList", async (request) => {
  if (!request.user) throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, "log in first.");
  const isMod = await new Parse.Query(Parse.Role).equalTo("name", "moderator").equalTo("users", request.user).first({ useMasterKey: true });
  if (!isMod) throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, "moderators only.");
  const notes = await new Parse.Query("Note").find({ useMasterKey: true });   // lock 4: the master key stays here
  return notes.map((n) => ({ id: n.id, text: n.get("text"), owner: n.get("owner")?.id }));
});

Medido: ana chamando moderatorList recebeu 119 moderators only.; bob, que está na role, recebeu a lista completa, donos incluídos. A classe em si continuou tão travada quanto antes.

A role foi criada por seed.sh com a master key, porque criar uma role é por si só uma escrita privilegiada; a classe _Role na dashboard mostra uma linha, moderator, com bob na relation users.

O useMasterKey: true nas duas queries é a trava 4 em uma linha: a master key é usada, dentro do backend, por código cujo deploy você fez, numa query que você escolheu. Nenhum cliente jamais a tem, e um moderador que sai da role perde a lista na próxima chamada.

Quais permissões uma classe nova recebe por padrão?

Todas, para todo mundo. Provamos isso uma segunda vez, de propósito: criamos uma classe Draft do jeito preguiçoso, escrevendo nela a partir de uma requisição anônima, e abrimos o Database Browser. Três linhas, e toda ACL diz Public Read + Write. A CLP era igual: tudo público, addField incluído.

O Database Browser mostrando uma classe Draft com três objetos, cada um com a coluna ACL lendo Public Read + Write, criados por requisições anônimas

Nada na dashboard avisa você. A classe simplesmente aparece, aberta, ao lado da que você travou uma hora atrás. Se a classe existe para que clientes criem linhas, a ordem é: crie a classe e a CLP primeiro (o Add Class da dashboard ou a schema API), faça o deploy do hook e só então deixe a primeira escrita do cliente acontecer.

O que acontece com as linhas criadas antes da trava?

Elas ficam com a ACL com que nasceram. Um hook beforeSave roda nos saves feitos depois do deploy, e as catorze notas que nossas sondas criaram enquanto a classe estava aberta ainda diziam Public Read + Write. Qualquer um com a chave ainda conseguia ler essas, com trava ou sem. O migrate-acl.sh do repositório percorre a classe uma vez com a master key:

# Stack: bash + curl (master key) | File: migrate-acl.sh (core loop)
# rows with an owner → owner-only ACL; ownerless rows (anonymous drive-bys) → deleted
if [ "$action" = fix ]; then
  curl -s -o /dev/null "${MK[@]}" -X PUT "$BASE/classes/Note/$id" -d "{\"ACL\":{\"$owner\":{\"read\":true,\"write\":true}}}"
else
  curl -s -o /dev/null "${MK[@]}" -X DELETE "$BASE/classes/Note/$id"
fi

Ele corrigiu toda linha com dono, apagou as sem dono (as notas de passagem que as sondas anônimas tinham criado) e reportou 14 linhas restantes, todas com dono. Rode uma vez, depois que o hook estiver no ar e provado, e mantenha no repositório: a próxima classe que você travar vai precisar dele também.

Como fica a tabela de doze requisições depois das quatro travas?

Este é o estado 2, medido às 15:33 UTC, depois que a CLP assentou e o hook e a role estavam no ar:

identity → request                              HTTP / Parse code
anonymous (JS key)  → list notes           404 / 101
anonymous (JS key)  → create note          404 / 101
ana (session token) → list notes           200
ana (session token) → read her note        200
ana (session token) → update her note      200
ana (session token) → create note          201
bob (session token) → read ana's note      404 / 101
bob (session token) → update ana's note    404 / 101
master key          → read ana's note      200
anonymous (JS key)  → delete ana's note    404 / 101
bob (session token) → delete ana's note    404 / 101
master key          → delete ana's note    200

Mesma chave na página, mesmas doze requisições. Seis delas agora falham, e cada uma das seis é uma requisição que seu app nunca deveria ter conseguido fazer. As quatro que passam para a ana são as linhas dela. As duas que passam para a master key rodam de uma máquina que você controla.

probe.sh imprime essa tabela em uma rodada. Mantenha-a ao lado da classe. Quando um recurso novo precisar de uma permissão nova, o diff entre duas rodadas é a revisão.

Quando um backend com chaves de cliente é o design errado?

Quando uma identidade nunca pode ler uma linha nem por acidente, e a regra é complexa demais para caber numa CLP mais uma ACL: dados multi-tenant com joins entre tenants, ou linhas cuja visibilidade depende de um cálculo.

Nesse caso o cliente não deveria consultar a classe de jeito nenhum. Deveria chamar uma Cloud Function que roda a query com a master key e devolve um resultado moldado, que é o que moderatorList já faz para um caso.

O outro caso é um segredo que o backend precisa guardar em nome do cliente: a chave de um provedor de pagamento, um token de terceiros. Esses vão no ambiente do Cloud Code ou num container, nunca numa classe que o cliente alcança, por mais travada que esteja.

Para uma classe de linhas que pertencem a usuários, as quatro travas bastam, e o conjunto todo é um PUT no schema, 20 linhas de Cloud Code e uma migração.

O que você acrescentaria ao lockdown lab em seguida?

  • Uma nota compartilhada. Adicione uma relation sharedWith e estenda o hook para conceder leitura a esses usuários. A sonda ganha uma linha: carol lê uma nota compartilhada com ela → 200.
  • Uma CLP por role. Dê à role moderator escrita na classe pelo campo Add Role da CLP e veja quais linhas do bob ficam verdes. Depois decida se você prefere isso à função.
  • A mesma sonda em _User. Por padrão, um usuário logado consegue ler as linhas de outros usuários. Trave _User com Enforce private users em Advanced Options (pago) ou com uma CLP, e sonde.
  • Uma verificação no deploy. Rode probe.sh no CI contra um backend de staging e falhe o build se alguma linha que deveria estar vermelha ficar verde.
  • Usuários primeiro. Se o app ainda não tem contas, esse é o post anterior: Como adicionar autenticação a um app web ou Flutter sem escrever um servidor de auth.

Tagssecuritypermissionsaclrolesclass-level-permissionsmaster-keyjavascriptparse-server

Perguntas frequentes

É seguro colocar o App ID e a JavaScript key no código do frontend?

Sim, é para isso que as chaves de cliente existem: elas identificam o app, não o usuário, e quem as tem só consegue fazer o que as permissões do backend permitem. O que nunca pode ir para o cliente é a master key, que ignora toda permissão. O risco não é a chave na página; é uma classe deixada com as permissões padrão, onde qualquer um com a chave consegue listar, editar e apagar todas as linhas. Medimos exatamente isso em 17 de setembro de 2026.

Qual é a diferença entre permissões por classe e uma ACL?

As permissões por classe (CLP) são a porta da classe: quem pode fazer find, get, create, update, delete ou add field, para começar. Uma ACL é uma lista por linha de quais usuários e roles podem ler ou escrever aquela linha. Uma requisição precisa passar pelas duas. No nosso teste, a CLP barrou as requisições anônimas (404, código 101) e a ACL só do dono escondeu a nota da ana do bob (também 404, código 101), enquanto a ana e a master key continuaram passando.

Por que a ACL deve ser definida por um hook de Cloud Code em vez de pelo cliente?

Porque o cliente pode esquecer, ou ser substituído por um comando curl que não se dá ao trabalho. Um hook beforeSave na classe roda no backend para toda linha nova, seja quem for que a enviou, e carimba uma ACL que concede leitura e escrita apenas a request.user. Linhas criadas antes do hook mantêm a ACL antiga, por isso o repositório inclui um script de migração para elas.

Quanto tempo uma mudança de permissão leva para ser aplicada?

Mais do que uma requisição. Depois que mudamos as permissões por classe pela schema API, requisições anônimas foram respondidas de forma inconsistente por 22 minutos: alguns servidores aplicaram a regra nova, um continuou aplicando a antiga, e Restart App não resolveu. Dois deploys de Cloud Code resolveram. Sonde até que zero de N requisições passem, não até a primeira negativa.

Como deixo um admin ler todas as linhas sem abrir a classe?

Com uma role e uma Cloud Function. A função verifica que request.user está na role (uma query de role com a master key), depois roda a query com useMasterKey dentro do backend e devolve as linhas. A master key nunca sai do servidor; quem não é membro recebe o erro 119. No nosso teste a ana recebeu 119 e o bob, moderador, recebeu a lista completa.

Leitura complementar

Revisado e verificado por Back4app Engineering, Editorial review na Back4app

Testado em 17 de setembro de 2026 — Backend: Back4app, plano Free, USA East · Sondas: bash + curl no macOS · Regras: Cloud Code com deploy pelo editor da dashboard.

Mais da Back4app Engineering

Backend · tutorial

Como adicionar autenticação a um app web ou Flutter sem escrever um servidor de auth

Containers · tutorial

How to Deploy a Next.js SSR App With Docker and a Backend [Full Repo]

Backend · tutorial

Build and Deploy a Node.js REST API in 15 Minutes — Without Owning a Server [Full Code Inside]